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 }}
# START: Setup Node and PNPM enabling cache
- name: Setup Node.js
uses: actions/setup-node@v6
- name: Setup pnpm + Node.js
uses: pnpm/setup@v2
with:
node-version-file: .nvmrc
- name: Enable PNPM
working-directory: ./plugins
shell: bash
run: |
corepack enable;
corepack install;
working-directory: plugins
runtime: node@24.21.0
install: false
- name: Get pnpm store path
id: pnpm-store

View File

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

View File

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

View File

@ -44,8 +44,6 @@ jobs:
- name: Lint
working-directory: ./common
run: |
corepack enable;
corepack install;
pnpm install;
pnpm run check-fmt:clj
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:
run:
@ -30,12 +52,18 @@ on:
required: true
default: '2'
base_url:
description: 'Penpot base URL for the (disabled) live Plugin API suite'
required: false
default: 'https://localhost:3449'
pull_request:
paths:
- 'frontend/**'
- 'common/**'
- 'render-wasm/**'
- '.github/workflows/tests-integration.yml'
- 'plugins/**'
- '.github/workflows/tests-e2e.yml'
types:
- opened
@ -51,18 +79,21 @@ on:
- 'frontend/**'
- 'common/**'
- 'render-wasm/**'
- '.github/workflows/tests-integration.yml'
- 'plugins/**'
- '.github/workflows/tests-e2e.yml'
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || inputs.gh_ref || github.ref }}
cancel-in-progress: true
jobs:
build-integration:
# ── 1. Build the frontend bundle once ──────────────────────────────────
build-bundle:
if: ${{ !github.event.pull_request.draft }}
name: "Build Integration Bundle"
runs-on: penpot-extended-runner
timeout-minutes: 30
container:
image: penpotapp/devenv:latest
volumes:
@ -85,26 +116,36 @@ jobs:
- name: Extract cache key
id: vars
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
if: steps.restore.outputs.cache-hit != 'true'
working-directory: ./frontend
run: |
./scripts/build
- name: Store Bundle Cache
uses: actions/cache@v5
if: steps.restore.outputs.cache-hit != 'true'
uses: actions/cache/save@v5
with:
key: ${{ steps.vars.outputs.bundle_key }}
path: frontend/resources/public
# ── 2. Consumers: restore the bundle, never rebuild it ─────────────────
test-integration:
if: ${{ !github.event.pull_request.draft }}
name: "Integration Tests (${{ matrix.shard }})"
runs-on: penpot-extended-runner
timeout-minutes: 40
needs: build-integration
needs: build-bundle
strategy:
fail-fast: false
@ -129,14 +170,13 @@ jobs:
- name: Restore Cache
uses: actions/cache/restore@v5
with:
key: ${{ needs.build-integration.outputs.bundle_key }}
key: ${{ needs.build-bundle.outputs.bundle_key }}
path: frontend/resources/public
fail-on-cache-miss: true
- name: Install deps
working-directory: ./frontend
run: |
corepack enable;
corepack install;
pnpm install --frozen-lockfile;
# No-op once the shared volume is warm; keeps the first run working.
@ -177,6 +217,89 @@ jobs:
if-no-files-found: ignore
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:
if: ${{ !cancelled() && !github.event.pull_request.draft && needs.test-integration.result != 'skipped' }}
name: "Merge Integration Reports"
@ -200,8 +323,6 @@ jobs:
- name: Install deps
working-directory: ./frontend
run: |
corepack enable;
corepack install;
pnpm install --frozen-lockfile;
- name: Download blob reports
@ -245,8 +366,53 @@ jobs:
- name: Upload HTML report
uses: actions/upload-artifact@v7
if: always()
with:
name: integration-html-report
path: frontend/playwright-report/
overwrite: true
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
working-directory: ./exporter
run: |
corepack enable;
corepack install;
pnpm install;
pnpm run check-fmt:clj
pnpm run lint:clj

View File

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

View File

@ -46,8 +46,6 @@ jobs:
- name: Lint
working-directory: ./library
run: |
corepack enable;
corepack install;
pnpm install;
pnpm run check-fmt
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
shell: bash
run: |
corepack enable;
corepack install;
pnpm install -r;
- 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 path from "path"
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({
description:
"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) {
const host = process.env.PENPOT_DB_HOST || "postgres"
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)
})
})
return executePsql(args.sql, args.test === true, context.worktree)
},
})
@ -82,69 +149,11 @@ const parenRepairTool = tool({
},
async execute(args, context) {
const script = path.join(context.worktree, "scripts/paren-repair")
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()
}
})
return executeParenRepair(args, context.worktree)
},
})
export default async function plugin() {
async function server() {
return {
tool: {
"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 }
},
})
})
}
// 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,
// },
// }
// }
export default {
id: "penpot",
setup,
server,
}

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.
- 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.
- 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
@ -37,4 +37,4 @@
- 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.
- 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 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
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)
- **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.
@ -55,7 +55,7 @@ module. You can read it from `mem:<MODULE>/core`
- `docker/` contains devenv related code, not needed unless specifically instructed.
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`.
- `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.

View File

@ -1,10 +1,10 @@
# 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
- `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.
- 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.
@ -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.
- 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.
## 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.
- 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.
- 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`.
## 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`.
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
@ -43,7 +43,7 @@ Container-internal ports fixed; host side offset `10000·N`.
| 14181 | 24181 | … | 14281 | Serena MCP |
| 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

View File

@ -138,13 +138,15 @@ Distinguishing abstractions (the OOP articulation of the shared principles):
control via Playwright; recipe in the README.
## CI
Headless per-PR gate: `.github/workflows/tests-composable-suite.yml` runs
`pnpm --filter composable-test-suite run test:ci` — mocked backend (frontend e2e static server +
Headless per-PR gate: the `composable-test-suite` job in
`.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`,
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
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 frontend bundle is built once per SHA and restored by this job (`mem:frontend/e2e-ci-workflow`).
Details: README, "Running in CI".
## Substrate

View File

@ -53,6 +53,7 @@ Diagnostics and validation:
- Source-edit compile/hot-reload diagnostics: `mem:frontend/compile-diagnostics`.
- Runtime crash recovery: `mem:frontend/handling-crashes`.
- 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`.
- 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.
## 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
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.
- **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).
- **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)
A native opencode tool `paren-repair` is available at `.opencode/scripts/paren-repair.ts`.
The LLM can call it directly with:
- `files`: Array of file paths to fix
A native opencode tool `paren-repair` 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:
- `files`: comma-separated file paths to fix (a string, not an array)
- `code`: Code string to fix via stdin
Example usage by the LLM:

View File

@ -27,7 +27,9 @@ scripts/psql --host myhost --user myuser --db mydb
## 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:
- `sql`: SQL command string to execute
- `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
integrity-verified, so it cannot go stale; staleness lives in
node_modules. Only `scripts/clean-node-modules --store` removes it.
- Every `package.json` (about 35 of them) must carry a `packageManager` field
with the identical `pnpm@<version>+sha512.<hash>` value. Do not let them drift.
- CI pins no pnpm version; workflows rely on corepack reading
`packageManager`. Fixing the fields fixes CI.
- Every first-party `package.json` (35 files: workspace roots plus
members) must carry a `packageManager` field with the identical
`pnpm@<version>+sha512.<hash>` value. Do not let them drift.
`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
1. Resolve the target tag first and note the version. Example:
`npm view pnpm dist-tags --json` for `next-12` (latest 12.x). The tag
moves over time; always re-check.
2. List every directory with a `package.json`, excluding `node_modules`
(`fd -H -t f package.json -E node_modules`). This list is the work set;
do not maintain a hand-written list.
3. Run `corepack use pnpm@<tag>` in workspace roots first, then members.
`corepack use` stamps `packageManager` in the nearest package.json and
runs an install. Member runs repeat the workspace install; after the root
run they are quick no-ops.
4. If a run fails, fix the cause (see gotchas) and re-run that directory.
1. Resolve the target version first and note it. Example:
`pnpm view pnpm dist-tags --json` for `latest-12` (latest 12.x). The
tag moves over time; always re-check.
2. Bump `PNPM_VERSION` (+ arch SHAs) in `docker/devenv/Dockerfile` and in
`docker/images/Dockerfile.{media-processor,exporter,mcp}`.
3. From the repo root, on a host whose system pnpm is the target version
(rebuilt devenv), run `scripts/sync-pnpm-version`. It resolves the
integrity hash via `pnpm view` + node (no npm, no corepack, no
python3) and stamps the identical field into all 35 files, replacing
only the value line (each file keeps its own indent) and inserting
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
- `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 placeholder scaffold into its `pnpm-workspace.yaml`:
`allowBuilds: esbuild: set this to true or false`. Current pnpm writes
@ -70,10 +83,12 @@ file (never pipe tool output through filters).
## Verification
- Every `packageManager` field is byte-identical (same version and hash).
- `pnpm --version` in each workspace prints the target version.
- `scripts/sync-pnpm-version --check` passes: every `packageManager`
field is byte-identical (same version and hash).
- `pnpm install --frozen-lockfile` succeeds in each of the 11 workspaces.
- `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

View File

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

131
backend/pnpm-lock.yaml generated
View File

@ -7,96 +7,153 @@ importers:
configDependencies: {}
packageManagerDependencies:
pnpm:
specifier: 12.3.4
version: 12.3.4
specifier: 12.5.1
version: 12.5.1
packages:
'@pnpm/exe.darwin-arm64@12.3.4':
resolution: {integrity: sha512-PAyUol8T1+/+ViOiXAt51ECA+QnfXCqz6foL4bW+LsoX0NcVd5XVEM2mRQu+LV4oc7uRz9zf9U0P+XFfuQeDAw==}
'@pnpm/exe.android-arm64@12.5.1':
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]
os: [darwin]
'@pnpm/exe.darwin-x64@12.3.4':
resolution: {integrity: sha512-fxP9JCk0Cdye+ePuj+GJJLMUMTqHGWRdb1dtv4How876uQ2ehxvenpgiYAir/ceO9PsYUZkFTtyZdx+rRu5QOA==}
'@pnpm/exe.darwin-x64@12.5.1':
resolution: {integrity: sha512-YHwI1g2B+Y2cAEylOR31r781GWijdJL7Gy7TEn/ZpZ5v/Cuwp5/Pz0EFCgo0M2MKLafAbL/laUMGvOpAdnerAw==}
cpu: [x64]
os: [darwin]
'@pnpm/exe.linux-arm64-musl@12.3.4':
resolution: {integrity: sha512-FBOt0/7ye6O6q4AllVV5QMviB6qE6fqkeczV/+MDWQsmo+QJrlfsh6X7CpH/tClVpBZEyIbjpUoT8bNhCYBxEg==}
'@pnpm/exe.freebsd-x64@12.5.1':
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]
os: [linux]
libc: [musl]
'@pnpm/exe.linux-arm64@12.3.4':
resolution: {integrity: sha512-t71AVA7LRqiKTyZ5xMYaZc2n5DfdpMbfokZuiIOXHBOM03ECnF0t4iYwaBDqJgVjlKYUOwaF/bRQajGNA4cJ4w==}
'@pnpm/exe.linux-arm64@12.5.1':
resolution: {integrity: sha512-6hbMWW2/eQo1b5wvQPe/ndLHVTH88Rav6N+ffD4/h6IuFMjWCgasFYYsFv0GrvpqREAx3SwmZSvli+0NFXqbxA==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@pnpm/exe.linux-x64-musl@12.3.4':
resolution: {integrity: sha512-RPmk7Jb/aYaFvL2iyDN/AtMY+hUEsue732WmXpcuQ9tBpMnGyA5py7Z3+e+qmQaJ0zY/4ni9jJiyPBQHujmv6w==}
'@pnpm/exe.linux-ppc64@12.5.1':
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]
os: [linux]
libc: [musl]
'@pnpm/exe.linux-x64@12.3.4':
resolution: {integrity: sha512-2ZqOlSPkfwX1h5cR+FPiWf8+F+2hZT/3TvhUK5sigHqwaQCIiq8R7CGxhndKs63JtcLi2a1Qpo+wX/EoyfjyJQ==}
'@pnpm/exe.linux-x64@12.5.1':
resolution: {integrity: sha512-3PkUBYo5z4dgtlnTNIFj7QGpcDUAuqXz9WGVigPDCeccEnhGiRkWmA5102TmYJHtwJP3LfmE+ZF9PGeWhn8p9Q==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@pnpm/exe.win32-arm64@12.3.4':
resolution: {integrity: sha512-ANyrHqyqco6SXBysUTRF74itDyyraea7IbFsKFdNXTjcFnfycTDx37EwuhdpPYFNSIh2JhUG4fByclsRfiHX7w==}
'@pnpm/exe.win32-arm64@12.5.1':
resolution: {integrity: sha512-49NvjZIXHly56/PLoEDD7VphbO4N8dwMXoApElDFCpVGDNGeKlwlib1tXtnpTF6i2/XIsAj7GgatHB5KHXgBnA==}
cpu: [arm64]
os: [win32]
'@pnpm/exe.win32-x64@12.3.4':
resolution: {integrity: sha512-WH/KqBPY/hq2Tb7SgQltEZytimcjgKRaCRL/aM9CI0c67iKc5TVmHUhIiL3Ux9FB4bWn36i6XewUcScQI+zG8w==}
'@pnpm/exe.win32-x64@12.5.1':
resolution: {integrity: sha512-ePUmQFPAPJFNMwg1lelDx2PdjHqYRiF0yfg5e6qeDbPNOu97QNo557Ru/SxmCrmt8ydQWNuZXWpdEp019md5vw==}
cpu: [x64]
os: [win32]
pnpm@12.3.4:
resolution: {integrity: sha512-lhqkH7B32joEpEHZ+OFevAyW2o73ELLrZ7+e58sGEOq9SPH9hfUc/+c4RnhfoPh8VqOocqHYk/hEZ0G1zORUVw==}
pnpm@12.5.1:
resolution: {integrity: sha512-4/MFvHhKK8ifWtO2E4iJRw+ujSr182thIW7JHCw9ZAiXdfneOKrDMQROpA8kXLDVZmOS399lgk4ZB+9qLGLeXw==}
engines: {node: '>=18.*'}
hasBin: true
snapshots:
'@pnpm/exe.darwin-arm64@12.3.4':
'@pnpm/exe.android-arm64@12.5.1':
optional: true
'@pnpm/exe.darwin-x64@12.3.4':
'@pnpm/exe.android-x64@12.5.1':
optional: true
'@pnpm/exe.linux-arm64-musl@12.3.4':
'@pnpm/exe.darwin-arm64@12.5.1':
optional: true
'@pnpm/exe.linux-arm64@12.3.4':
'@pnpm/exe.darwin-x64@12.5.1':
optional: true
'@pnpm/exe.linux-x64-musl@12.3.4':
'@pnpm/exe.freebsd-x64@12.5.1':
optional: true
'@pnpm/exe.linux-x64@12.3.4':
'@pnpm/exe.linux-arm64-musl@12.5.1':
optional: true
'@pnpm/exe.win32-arm64@12.3.4':
'@pnpm/exe.linux-arm64@12.5.1':
optional: true
'@pnpm/exe.win32-x64@12.3.4':
'@pnpm/exe.linux-ppc64@12.5.1':
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:
'@pnpm/exe.darwin-arm64': 12.3.4
'@pnpm/exe.darwin-x64': 12.3.4
'@pnpm/exe.linux-arm64': 12.3.4
'@pnpm/exe.linux-arm64-musl': 12.3.4
'@pnpm/exe.linux-x64': 12.3.4
'@pnpm/exe.linux-x64-musl': 12.3.4
'@pnpm/exe.win32-arm64': 12.3.4
'@pnpm/exe.win32-x64': 12.3.4
'@pnpm/exe.android-arm64': 12.5.1
'@pnpm/exe.android-x64': 12.5.1
'@pnpm/exe.darwin-arm64': 12.5.1
'@pnpm/exe.darwin-x64': 12.5.1
'@pnpm/exe.freebsd-x64': 12.5.1
'@pnpm/exe.linux-arm64': 12.5.1
'@pnpm/exe.linux-arm64-musl': 12.5.1
'@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'

View File

@ -97,13 +97,31 @@ export JAVA_OPTS="\
--enable-native-access=ALL-UNNAMED \
--add-opens=java.base/java.nio=ALL-UNNAMED";
function setup_minio() {
function setup_s3_bucket() {
if [ "${PENPOT_OBJECTS_STORAGE_BACKEND}" != "s3" ]; then
return 0
fi
# Shared MinIO user/policy provisioning is handled by docker-compose.infra.yml.
# Per process startup only ensures that the configured bucket exists.
mc alias set penpot-s3/ "${PENPOT_OBJECTS_STORAGE_S3_ENDPOINT}" minioadmin minioadmin -q
mc mb "penpot-s3/${PENPOT_OBJECTS_STORAGE_S3_BUCKET}" -p -q
local attempts=0
local endpoint="${PENPOT_OBJECTS_STORAGE_S3_ENDPOINT}"
local bucket="${PENPOT_OBJECTS_STORAGE_S3_BUCKET}"
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;
fi
# Initialize MINIO config
setup_minio;
# Ensure the object storage bucket exists.
setup_s3_bucket;
export JAVA_OPTS="$JAVA_OPTS -Dlog4j2.configurationFile=log4j2-devenv-repl.xml"
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;
fi
# Ensure the object storage bucket exists.
setup_s3_bucket;
export OPTIONS="-A:dev"
entrypoint=${1:-app.main};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -166,6 +166,33 @@
(t/is (th/ex-info? err))
(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
(let [profile (th/create-profile* 1)
file (th/create-file* 1 {:profile-id (:id profile)

View File

@ -138,6 +138,66 @@
(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
(let [prof (th/create-profile* 1)
proj (th/create-project* 1 {:profile-id (:id prof)
@ -1262,3 +1322,48 @@
(t/is (th/ex-info? error))
(t/is (= :not-found (:type 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-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
(let [profile1 (th/create-profile* 1)
profile2 (th/create-profile* 2)

View File

@ -1116,6 +1116,62 @@
out (th/command! data)]
(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
(with-mocks [mock {:target 'app.email/send! :return nil}]
(let [profile1 (th/create-profile* 1 {:is-active true})

View File

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

131
common/pnpm-lock.yaml generated
View File

@ -7,96 +7,153 @@ importers:
configDependencies: {}
packageManagerDependencies:
pnpm:
specifier: 12.3.4
version: 12.3.4
specifier: 12.5.1
version: 12.5.1
packages:
'@pnpm/exe.darwin-arm64@12.3.4':
resolution: {integrity: sha512-PAyUol8T1+/+ViOiXAt51ECA+QnfXCqz6foL4bW+LsoX0NcVd5XVEM2mRQu+LV4oc7uRz9zf9U0P+XFfuQeDAw==}
'@pnpm/exe.android-arm64@12.5.1':
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]
os: [darwin]
'@pnpm/exe.darwin-x64@12.3.4':
resolution: {integrity: sha512-fxP9JCk0Cdye+ePuj+GJJLMUMTqHGWRdb1dtv4How876uQ2ehxvenpgiYAir/ceO9PsYUZkFTtyZdx+rRu5QOA==}
'@pnpm/exe.darwin-x64@12.5.1':
resolution: {integrity: sha512-YHwI1g2B+Y2cAEylOR31r781GWijdJL7Gy7TEn/ZpZ5v/Cuwp5/Pz0EFCgo0M2MKLafAbL/laUMGvOpAdnerAw==}
cpu: [x64]
os: [darwin]
'@pnpm/exe.linux-arm64-musl@12.3.4':
resolution: {integrity: sha512-FBOt0/7ye6O6q4AllVV5QMviB6qE6fqkeczV/+MDWQsmo+QJrlfsh6X7CpH/tClVpBZEyIbjpUoT8bNhCYBxEg==}
'@pnpm/exe.freebsd-x64@12.5.1':
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]
os: [linux]
libc: [musl]
'@pnpm/exe.linux-arm64@12.3.4':
resolution: {integrity: sha512-t71AVA7LRqiKTyZ5xMYaZc2n5DfdpMbfokZuiIOXHBOM03ECnF0t4iYwaBDqJgVjlKYUOwaF/bRQajGNA4cJ4w==}
'@pnpm/exe.linux-arm64@12.5.1':
resolution: {integrity: sha512-6hbMWW2/eQo1b5wvQPe/ndLHVTH88Rav6N+ffD4/h6IuFMjWCgasFYYsFv0GrvpqREAx3SwmZSvli+0NFXqbxA==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@pnpm/exe.linux-x64-musl@12.3.4':
resolution: {integrity: sha512-RPmk7Jb/aYaFvL2iyDN/AtMY+hUEsue732WmXpcuQ9tBpMnGyA5py7Z3+e+qmQaJ0zY/4ni9jJiyPBQHujmv6w==}
'@pnpm/exe.linux-ppc64@12.5.1':
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]
os: [linux]
libc: [musl]
'@pnpm/exe.linux-x64@12.3.4':
resolution: {integrity: sha512-2ZqOlSPkfwX1h5cR+FPiWf8+F+2hZT/3TvhUK5sigHqwaQCIiq8R7CGxhndKs63JtcLi2a1Qpo+wX/EoyfjyJQ==}
'@pnpm/exe.linux-x64@12.5.1':
resolution: {integrity: sha512-3PkUBYo5z4dgtlnTNIFj7QGpcDUAuqXz9WGVigPDCeccEnhGiRkWmA5102TmYJHtwJP3LfmE+ZF9PGeWhn8p9Q==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@pnpm/exe.win32-arm64@12.3.4':
resolution: {integrity: sha512-ANyrHqyqco6SXBysUTRF74itDyyraea7IbFsKFdNXTjcFnfycTDx37EwuhdpPYFNSIh2JhUG4fByclsRfiHX7w==}
'@pnpm/exe.win32-arm64@12.5.1':
resolution: {integrity: sha512-49NvjZIXHly56/PLoEDD7VphbO4N8dwMXoApElDFCpVGDNGeKlwlib1tXtnpTF6i2/XIsAj7GgatHB5KHXgBnA==}
cpu: [arm64]
os: [win32]
'@pnpm/exe.win32-x64@12.3.4':
resolution: {integrity: sha512-WH/KqBPY/hq2Tb7SgQltEZytimcjgKRaCRL/aM9CI0c67iKc5TVmHUhIiL3Ux9FB4bWn36i6XewUcScQI+zG8w==}
'@pnpm/exe.win32-x64@12.5.1':
resolution: {integrity: sha512-ePUmQFPAPJFNMwg1lelDx2PdjHqYRiF0yfg5e6qeDbPNOu97QNo557Ru/SxmCrmt8ydQWNuZXWpdEp019md5vw==}
cpu: [x64]
os: [win32]
pnpm@12.3.4:
resolution: {integrity: sha512-lhqkH7B32joEpEHZ+OFevAyW2o73ELLrZ7+e58sGEOq9SPH9hfUc/+c4RnhfoPh8VqOocqHYk/hEZ0G1zORUVw==}
pnpm@12.5.1:
resolution: {integrity: sha512-4/MFvHhKK8ifWtO2E4iJRw+ujSr182thIW7JHCw9ZAiXdfneOKrDMQROpA8kXLDVZmOS399lgk4ZB+9qLGLeXw==}
engines: {node: '>=18.*'}
hasBin: true
snapshots:
'@pnpm/exe.darwin-arm64@12.3.4':
'@pnpm/exe.android-arm64@12.5.1':
optional: true
'@pnpm/exe.darwin-x64@12.3.4':
'@pnpm/exe.android-x64@12.5.1':
optional: true
'@pnpm/exe.linux-arm64-musl@12.3.4':
'@pnpm/exe.darwin-arm64@12.5.1':
optional: true
'@pnpm/exe.linux-arm64@12.3.4':
'@pnpm/exe.darwin-x64@12.5.1':
optional: true
'@pnpm/exe.linux-x64-musl@12.3.4':
'@pnpm/exe.freebsd-x64@12.5.1':
optional: true
'@pnpm/exe.linux-x64@12.3.4':
'@pnpm/exe.linux-arm64-musl@12.5.1':
optional: true
'@pnpm/exe.win32-arm64@12.3.4':
'@pnpm/exe.linux-arm64@12.5.1':
optional: true
'@pnpm/exe.win32-x64@12.3.4':
'@pnpm/exe.linux-ppc64@12.5.1':
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:
'@pnpm/exe.darwin-arm64': 12.3.4
'@pnpm/exe.darwin-x64': 12.3.4
'@pnpm/exe.linux-arm64': 12.3.4
'@pnpm/exe.linux-arm64-musl': 12.3.4
'@pnpm/exe.linux-x64': 12.3.4
'@pnpm/exe.linux-x64-musl': 12.3.4
'@pnpm/exe.win32-arm64': 12.3.4
'@pnpm/exe.win32-x64': 12.3.4
'@pnpm/exe.android-arm64': 12.5.1
'@pnpm/exe.android-x64': 12.5.1
'@pnpm/exe.darwin-arm64': 12.5.1
'@pnpm/exe.darwin-x64': 12.5.1
'@pnpm/exe.freebsd-x64': 12.5.1
'@pnpm/exe.linux-arm64': 12.5.1
'@pnpm/exe.linux-arm64-musl': 12.5.1
'@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'

View File

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

View File

@ -448,6 +448,21 @@
::oapi/type "string"
::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.
;; Local part: valid RFC chars, no leading/trailing dot, no consecutive dots.
;; Domain: labels can't start/end with hyphen, no empty labels.

View File

@ -70,6 +70,20 @@
[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
[id]
#?(:clj (.getMostSignificantBits ^UUID id)

View File

@ -9,6 +9,7 @@
[app.common.data :as d]
[app.common.schema :as sm]
[app.common.schema.generators :as sg]
[app.common.uuid :as uuid]
[clojure.test :as t]))
(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 "userdomain.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 2) (aget parts 2)))
(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
ENV NODE_VERSION=v24.20.0 \
ENV NODE_VERSION=v24.21.0 \
PNPM_VERSION=12.5.1 \
PATH=/opt/node/bin:$PATH
RUN set -eux; \
@ -75,10 +76,14 @@ RUN set -eux; \
aarch64|arm64) \
OPENSSL_ARCH='linux-aarch64'; \
BINARY_URL="https://nodejs.org/dist/${NODE_VERSION}/node-${NODE_VERSION}-linux-arm64.tar.gz"; \
PNPM_ARCH='arm64'; \
PNPM_SHA256='84e1290e82c800acd406b6db27e9650e15db3c2344d12162cc305ea1f942c6ff'; \
;; \
amd64|x86_64) \
OPENSSL_ARCH='linux-x86_64'; \
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}"; \
@ -89,10 +94,14 @@ RUN set -eux; \
mkdir -p /opt/node; \
cd /opt/node; \
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; \
find /opt/node/include/node/openssl/archs -mindepth 1 -maxdepth 1 ! -name "$OPENSSL_ARCH" -exec rm -rf {} \; ; \
corepack enable; \
rm -rf /tmp/nodejs.tar.gz;
rm -f /tmp/nodejs.tar.gz /tmp/pnpm.tar.gz;
################################################################################
## OPENCODE SETUP
@ -100,7 +109,7 @@ RUN set -eux; \
FROM base AS setup-opencode
ENV OPENCODE_VERSION=1.18.25
ENV OPENCODE_VERSION=1.18.31
RUN set -ex; \
ARCH="$(dpkg --print-architecture)"; \
@ -121,6 +130,37 @@ RUN set -ex; \
chmod +x /tmp/opencode/opencode; \
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
@ -166,12 +206,12 @@ RUN set -eux; \
ARCH="$(dpkg --print-architecture)"; \
case "${ARCH}" in \
aarch64|arm64) \
ESUM='5b222fce0b7076a10ac7ae3b1009a6c2caf4f35bc4e81de72010af6750c5e146'; \
BINARY_URL='https://cdn.azul.com/zulu/bin/zulu26.32.13-ca-jdk26.0.2-linux_aarch64.tar.gz'; \
ESUM='9fa5bf865783c43840101fcdc3a5222b60daf4fe11f26806247bf807c08aba38'; \
BINARY_URL='https://cdn.azul.com/zulu/bin/zulu27.28.101-ca-jdk27.0.0-linux_aarch64.tar.gz'; \
;; \
amd64|x86_64) \
ESUM='4b7c114917aebd0fc6284fc7111245d7747a4d9603bd12d86b384b1abc9d575d'; \
BINARY_URL='https://cdn.azul.com/zulu/bin/zulu26.32.13-ca-jdk26.0.2-linux_x64.tar.gz'; \
ESUM='ccbc15c4edbedfdcc03c2d29a2aa2c6daf9e6ffb4cda7dd5ece0fcbb37350267'; \
BINARY_URL='https://cdn.azul.com/zulu/bin/zulu27.28.101-ca-jdk27.0.0-linux_x64.tar.gz'; \
;; \
*) \
echo "Unsupported arch: ${ARCH}"; \
@ -242,16 +282,17 @@ RUN set -eux; \
FROM base AS setup-utils
ENV CLJKONDO_VERSION=2026.07.24 \
BABASHKA_VERSION=1.13.219 \
ENV CLJKONDO_VERSION=2026.08.04 \
BABASHKA_VERSION=1.13.223 \
CLJFMT_VERSION=0.16.5 \
PIXI_VERSION=0.75.0 \
GITHUB_CLI_VERSION=2.97.0 \
UV_VERSION=0.12.1 \
PIXI_VERSION=0.81.0 \
GITHUB_CLI_VERSION=2.101.0 \
AWS_CLI_VERSION=2.36.49 \
UV_VERSION=0.12.17 \
UV_TOOL_DIR=/opt/uv/tools \
UV_TOOL_BIN_DIR=/opt/utils/bin \
UV_PYTHON_INSTALL_DIR=/opt/uv/python \
SERENA_VERSION=1.6.1
SERENA_VERSION=1.7.0
RUN set -ex; \
ARCH="$(dpkg --print-architecture)"; \
@ -356,24 +397,30 @@ RUN set -ex; \
tar -xv --strip-components=1 -f /tmp/gh.tar.gz; \
rm -rf /tmp/gh.tar.gz;
# Install minio client
# Install AWS CLI
RUN set -ex; \
ARCH="$(dpkg --print-architecture)"; \
case "${ARCH}" in \
aarch64|arm64) \
BINARY_URL="https://dl.min.io/client/mc/release/linux-arm64/mc"; \
BINARY_ARCH="aarch64"; \
BINARY_SHA256="2dec26fdaff2f5e34961ea98f456297104f73d8409fc3599c8f2909c6e27a07e"; \
;; \
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}"; \
exit 1; \
;; \
esac; \
wget -O /tmp/mc ${BINARY_URL}; \
mv /tmp/mc /opt/utils/bin/; \
chmod +x /opt/utils/bin/mc;
BINARY_URL="https://awscli.amazonaws.com/awscli-exe-linux-${BINARY_ARCH}-${AWS_CLI_VERSION}.zip"; \
curl -LfsSo /tmp/awscliv2.zip ${BINARY_URL}; \
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
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; \
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 -qqy install postgresql-client-16; \
apt-get -qqy install postgresql-client-18; \
apt-get clean; \
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-caddy /usr/bin/caddy /usr/bin/caddy
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-mime.types /etc/nginx/mime.types
@ -560,4 +608,3 @@ COPY files/init.sh /home/init.sh
ENTRYPOINT ["/home/entrypoint.sh"]
CMD ["/home/init.sh"]

View File

@ -10,11 +10,12 @@
# Container names and volume names. Volumes are pinned by explicit name
# (rather than relying on COMPOSE_PROJECT_NAME prefixing) so the physical
# volumes survive project renames without a data migration. ws0 reuses the
# pre-Stage-2 physical volume names (penpotdev_*).
# volumes survive project renames. Replacement services use new volume names
# so data from the previous service remains available for rollback.
PENPOT_MAIN_CONTAINER_NAME=penpot-devenv-ws0-main
PENPOT_POSTGRES_DATA_VOLUME=penpotdev_postgres_data_pg16
PENPOT_MINIO_DATA_VOLUME=penpotdev_minio_data
PENPOT_POSTGRES_DATA_VOLUME=penpotdev_postgres_data_pg18
PENPOT_RUSTFS_DATA_VOLUME=penpotdev_rustfs_data
PENPOT_MAILPIT_DATA_VOLUME=penpotdev_mailpit_data
PENPOT_USER_DATA_VOLUME=penpotdev_user_data
# 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_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_S3_ENDPOINT=http://minio:9000
PENPOT_OBJECTS_STORAGE_S3_ENDPOINT=http://rustfs:9000
PENPOT_OBJECTS_STORAGE_S3_BUCKET=penpot
PENPOT_OBJECTS_STORAGE_S3_REGION=us-east-1
AWS_ACCESS_KEY_ID=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
# container are exposed; everything else (frontend dev server, backend API,

View File

@ -4,53 +4,47 @@ networks:
external: true
volumes:
postgres_data_pg16:
postgres_data_pg18:
name: ${PENPOT_POSTGRES_DATA_VOLUME}
minio_data:
name: ${PENPOT_MINIO_DATA_VOLUME}
rustfs_data:
name: ${PENPOT_RUSTFS_DATA_VOLUME}
mailpit_data:
name: ${PENPOT_MAILPIT_DATA_VOLUME}
valkey_data:
name: penpotdev_valkey_data
services:
minio:
image: "minio/minio:RELEASE.2025-04-03T14-56-28Z"
command: minio server /mnt/data --console-address ":9001"
rustfs:
image: "rustfs/rustfs:1.0.0"
command: /data
volumes:
- "minio_data:/mnt/data"
- "rustfs_data:/data"
environment:
- MINIO_ROOT_USER=minioadmin
- MINIO_ROOT_PASSWORD=minioadmin
- RUSTFS_ACCESS_KEY=${AWS_ACCESS_KEY_ID}
- 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:
default:
aliases:
- minio
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:
- rustfs
postgres:
image: postgres:16.8
image: postgres:18.6
command: postgres -c config_file=/etc/postgresql.conf
restart: always
stop_signal: SIGINT
@ -62,7 +56,12 @@ services:
volumes:
- ./files/postgresql.conf:/etc/postgresql.conf: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:
default:
aliases:
@ -80,12 +79,24 @@ services:
- valkey
mailer:
image: sj26/mailcatcher:latest
image: axllent/mailpit:v1.31.1
restart: always
volumes:
- "mailpit_data:/data"
environment:
- MP_DATABASE=/data/mailpit.db
- MP_MAX_MESSAGES=5000
- MP_DISABLE_VERSION_CHECK=true
expose:
- '1025'
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:
default:

View File

@ -84,8 +84,10 @@ services:
- PENPOT_OBJECTS_STORAGE_BACKEND=${PENPOT_OBJECTS_STORAGE_BACKEND}
- PENPOT_OBJECTS_STORAGE_S3_ENDPOINT=${PENPOT_OBJECTS_STORAGE_S3_ENDPOINT}
- 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_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY}
- AWS_DEFAULT_REGION=${AWS_DEFAULT_REGION}
- PENPOT_TENANT=${PENPOT_TENANT}
- PENPOT_TMUX_ATTACH=${PENPOT_TMUX_ATTACH}

View File

@ -1,2 +1,2 @@
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
# DHI Node image installs Node at the system level (symlinked into
# /usr/bin), so `./setup`'s internal `corepack enable` needs root to write
# there. Ownership is fixed right after.
# pnpm ships as a system binary (same tarball + SHA pin as
# docker/devenv/Dockerfile); the generated ./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; \
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
USER penpot:penpot

View File

@ -8,6 +8,36 @@ COPY $BUNDLE_PATH /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

View File

@ -4,6 +4,7 @@ LABEL maintainer="Penpot <docker@penpot.app>"
ENV LANG=en_US.UTF-8 \
LC_ALL=en_US.UTF-8 \
NODE_VERSION=v24.20.0 \
PNPM_VERSION=12.5.1 \
DEBIAN_FRONTEND=noninteractive \
PATH=/opt/node/bin:$PATH
@ -56,9 +57,13 @@ RUN set -eux; \
case "${ARCH}" in \
aarch64|arm64) \
BINARY_URL="https://nodejs.org/dist/${NODE_VERSION}/node-${NODE_VERSION}-linux-arm64.tar.gz"; \
PNPM_ARCH='arm64'; \
PNPM_SHA256='84e1290e82c800acd406b6db27e9650e15db3c2344d12162cc305ea1f942c6ff'; \
;; \
amd64|x86_64) \
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}"; \
@ -71,7 +76,13 @@ RUN set -eux; \
tar -xf /tmp/nodejs.tar.gz --strip-components=1; \
chown -R root /opt/node; \
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; \
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
nvm install
nvm use
corepack enable
pnpm install
```

View File

@ -39,5 +39,5 @@
"markdown-it-anchor": "^9.2.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: {}
packageManagerDependencies:
pnpm:
specifier: 12.3.4
version: 12.3.4
specifier: 12.5.1
version: 12.5.1
packages:
'@pnpm/exe.darwin-arm64@12.3.4':
resolution: {integrity: sha512-PAyUol8T1+/+ViOiXAt51ECA+QnfXCqz6foL4bW+LsoX0NcVd5XVEM2mRQu+LV4oc7uRz9zf9U0P+XFfuQeDAw==}
'@pnpm/exe.android-arm64@12.5.1':
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]
os: [darwin]
'@pnpm/exe.darwin-x64@12.3.4':
resolution: {integrity: sha512-fxP9JCk0Cdye+ePuj+GJJLMUMTqHGWRdb1dtv4How876uQ2ehxvenpgiYAir/ceO9PsYUZkFTtyZdx+rRu5QOA==}
'@pnpm/exe.darwin-x64@12.5.1':
resolution: {integrity: sha512-YHwI1g2B+Y2cAEylOR31r781GWijdJL7Gy7TEn/ZpZ5v/Cuwp5/Pz0EFCgo0M2MKLafAbL/laUMGvOpAdnerAw==}
cpu: [x64]
os: [darwin]
'@pnpm/exe.linux-arm64-musl@12.3.4':
resolution: {integrity: sha512-FBOt0/7ye6O6q4AllVV5QMviB6qE6fqkeczV/+MDWQsmo+QJrlfsh6X7CpH/tClVpBZEyIbjpUoT8bNhCYBxEg==}
'@pnpm/exe.freebsd-x64@12.5.1':
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]
os: [linux]
libc: [musl]
'@pnpm/exe.linux-arm64@12.3.4':
resolution: {integrity: sha512-t71AVA7LRqiKTyZ5xMYaZc2n5DfdpMbfokZuiIOXHBOM03ECnF0t4iYwaBDqJgVjlKYUOwaF/bRQajGNA4cJ4w==}
'@pnpm/exe.linux-arm64@12.5.1':
resolution: {integrity: sha512-6hbMWW2/eQo1b5wvQPe/ndLHVTH88Rav6N+ffD4/h6IuFMjWCgasFYYsFv0GrvpqREAx3SwmZSvli+0NFXqbxA==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@pnpm/exe.linux-x64-musl@12.3.4':
resolution: {integrity: sha512-RPmk7Jb/aYaFvL2iyDN/AtMY+hUEsue732WmXpcuQ9tBpMnGyA5py7Z3+e+qmQaJ0zY/4ni9jJiyPBQHujmv6w==}
'@pnpm/exe.linux-ppc64@12.5.1':
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]
os: [linux]
libc: [musl]
'@pnpm/exe.linux-x64@12.3.4':
resolution: {integrity: sha512-2ZqOlSPkfwX1h5cR+FPiWf8+F+2hZT/3TvhUK5sigHqwaQCIiq8R7CGxhndKs63JtcLi2a1Qpo+wX/EoyfjyJQ==}
'@pnpm/exe.linux-x64@12.5.1':
resolution: {integrity: sha512-3PkUBYo5z4dgtlnTNIFj7QGpcDUAuqXz9WGVigPDCeccEnhGiRkWmA5102TmYJHtwJP3LfmE+ZF9PGeWhn8p9Q==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@pnpm/exe.win32-arm64@12.3.4':
resolution: {integrity: sha512-ANyrHqyqco6SXBysUTRF74itDyyraea7IbFsKFdNXTjcFnfycTDx37EwuhdpPYFNSIh2JhUG4fByclsRfiHX7w==}
'@pnpm/exe.win32-arm64@12.5.1':
resolution: {integrity: sha512-49NvjZIXHly56/PLoEDD7VphbO4N8dwMXoApElDFCpVGDNGeKlwlib1tXtnpTF6i2/XIsAj7GgatHB5KHXgBnA==}
cpu: [arm64]
os: [win32]
'@pnpm/exe.win32-x64@12.3.4':
resolution: {integrity: sha512-WH/KqBPY/hq2Tb7SgQltEZytimcjgKRaCRL/aM9CI0c67iKc5TVmHUhIiL3Ux9FB4bWn36i6XewUcScQI+zG8w==}
'@pnpm/exe.win32-x64@12.5.1':
resolution: {integrity: sha512-ePUmQFPAPJFNMwg1lelDx2PdjHqYRiF0yfg5e6qeDbPNOu97QNo557Ru/SxmCrmt8ydQWNuZXWpdEp019md5vw==}
cpu: [x64]
os: [win32]
pnpm@12.3.4:
resolution: {integrity: sha512-lhqkH7B32joEpEHZ+OFevAyW2o73ELLrZ7+e58sGEOq9SPH9hfUc/+c4RnhfoPh8VqOocqHYk/hEZ0G1zORUVw==}
pnpm@12.5.1:
resolution: {integrity: sha512-4/MFvHhKK8ifWtO2E4iJRw+ujSr182thIW7JHCw9ZAiXdfneOKrDMQROpA8kXLDVZmOS399lgk4ZB+9qLGLeXw==}
engines: {node: '>=18.*'}
hasBin: true
snapshots:
'@pnpm/exe.darwin-arm64@12.3.4':
'@pnpm/exe.android-arm64@12.5.1':
optional: true
'@pnpm/exe.darwin-x64@12.3.4':
'@pnpm/exe.android-x64@12.5.1':
optional: true
'@pnpm/exe.linux-arm64-musl@12.3.4':
'@pnpm/exe.darwin-arm64@12.5.1':
optional: true
'@pnpm/exe.linux-arm64@12.3.4':
'@pnpm/exe.darwin-x64@12.5.1':
optional: true
'@pnpm/exe.linux-x64-musl@12.3.4':
'@pnpm/exe.freebsd-x64@12.5.1':
optional: true
'@pnpm/exe.linux-x64@12.3.4':
'@pnpm/exe.linux-arm64-musl@12.5.1':
optional: true
'@pnpm/exe.win32-arm64@12.3.4':
'@pnpm/exe.linux-arm64@12.5.1':
optional: true
'@pnpm/exe.win32-x64@12.3.4':
'@pnpm/exe.linux-ppc64@12.5.1':
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:
'@pnpm/exe.darwin-arm64': 12.3.4
'@pnpm/exe.darwin-x64': 12.3.4
'@pnpm/exe.linux-arm64': 12.3.4
'@pnpm/exe.linux-arm64-musl': 12.3.4
'@pnpm/exe.linux-x64': 12.3.4
'@pnpm/exe.linux-x64-musl': 12.3.4
'@pnpm/exe.win32-arm64': 12.3.4
'@pnpm/exe.win32-x64': 12.3.4
'@pnpm/exe.android-arm64': 12.5.1
'@pnpm/exe.android-x64': 12.5.1
'@pnpm/exe.darwin-arm64': 12.5.1
'@pnpm/exe.darwin-x64': 12.5.1
'@pnpm/exe.freebsd-x64': 12.5.1
'@pnpm/exe.linux-arm64': 12.5.1
'@pnpm/exe.linux-arm64-musl': 12.5.1
'@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'

View File

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

View File

@ -62,7 +62,7 @@ See the dedicated section [Agentic Dev Environment](../agentic-devenv/) for deta
### Parallel workspaces
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.
- `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/`
@ -168,7 +168,7 @@ the container's data volume regardless of this flag.
### 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
keeps msgbus Pub/Sub channels (collab broadcasts, team-org notifications,
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
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
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
completion. The "ws0 only" policy avoids multi-instance worker races (cron
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
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`.
- `docker/devenv/docker-compose.main.yml` (one main container + its Valkey)
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`)
layout, leftover containers and the auto-generated `penpotdev_default`
network must be removed before bringing the new ws0 instance up. The named
data volumes (`penpotdev_postgres_data_pg16`, `penpotdev_minio_data`,
`penpotdev_user_data`, `penpotdev_valkey_data`) are pinned by explicit
`name:` entries in the new compose files and are preserved through the
transition — your Postgres DB, MinIO objects, and home cache survive.
data volumes (`penpotdev_postgres_data_pg18`, `penpotdev_rustfs_data`,
`penpotdev_mailpit_data`, `penpotdev_user_data`, `penpotdev_valkey_data`) are
pinned by explicit `name:` entries in the new compose files. The legacy
`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:
@ -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
To test email sending, the devenv includes [MailCatcher](https://mailcatcher.me/),
a SMTP server that is used for develop. It does not send any mail outbounds.
Instead, it stores them in memory and allows to browse them via a web interface
similar to a webmail client. Simply navigate to:
To test email sending, the devenv includes
[Mailpit](https://mailpit.axllent.org/), an SMTP server for development. It does
not send mail externally. Instead, it stores messages in a persistent Docker
volume and provides a webmail-like interface. Simply navigate to:
[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
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).
> 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

View File

@ -4,7 +4,7 @@
"license": "MPL-2.0",
"author": "Kaleidos INC Sucursal en España SL",
"private": true,
"packageManager": "pnpm@12.3.4+sha512.961aa41fb077da3a04a441d9f8e15ebc0c96da8ef710b2eb67bf9ee7cb0610eabd48f1fd85f51cffe73846785fa0f87c56a3a872a1d893f8446741b5cce45457",
"packageManager": "pnpm@12.5.1+sha512.e3f305bc784a2bc89f5ad3b6138889470fae8d2af5f36b61216ec91c2c3d64089775f9de38aac331044ea40f245cb0d5666392dfdf65824e1907ef6a2c62de5f",
"repository": {
"type": "git",
"url": "https://github.com/penpot/penpot"
@ -30,7 +30,8 @@
},
"scripts": {
"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",
"build:app": "clojure -M:dev:shadow-cljs release main",
"build:wasm": "../render-wasm/build export",

131
exporter/pnpm-lock.yaml generated
View File

@ -7,96 +7,153 @@ importers:
configDependencies: {}
packageManagerDependencies:
pnpm:
specifier: 12.3.4
version: 12.3.4
specifier: 12.5.1
version: 12.5.1
packages:
'@pnpm/exe.darwin-arm64@12.3.4':
resolution: {integrity: sha512-PAyUol8T1+/+ViOiXAt51ECA+QnfXCqz6foL4bW+LsoX0NcVd5XVEM2mRQu+LV4oc7uRz9zf9U0P+XFfuQeDAw==}
'@pnpm/exe.android-arm64@12.5.1':
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]
os: [darwin]
'@pnpm/exe.darwin-x64@12.3.4':
resolution: {integrity: sha512-fxP9JCk0Cdye+ePuj+GJJLMUMTqHGWRdb1dtv4How876uQ2ehxvenpgiYAir/ceO9PsYUZkFTtyZdx+rRu5QOA==}
'@pnpm/exe.darwin-x64@12.5.1':
resolution: {integrity: sha512-YHwI1g2B+Y2cAEylOR31r781GWijdJL7Gy7TEn/ZpZ5v/Cuwp5/Pz0EFCgo0M2MKLafAbL/laUMGvOpAdnerAw==}
cpu: [x64]
os: [darwin]
'@pnpm/exe.linux-arm64-musl@12.3.4':
resolution: {integrity: sha512-FBOt0/7ye6O6q4AllVV5QMviB6qE6fqkeczV/+MDWQsmo+QJrlfsh6X7CpH/tClVpBZEyIbjpUoT8bNhCYBxEg==}
'@pnpm/exe.freebsd-x64@12.5.1':
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]
os: [linux]
libc: [musl]
'@pnpm/exe.linux-arm64@12.3.4':
resolution: {integrity: sha512-t71AVA7LRqiKTyZ5xMYaZc2n5DfdpMbfokZuiIOXHBOM03ECnF0t4iYwaBDqJgVjlKYUOwaF/bRQajGNA4cJ4w==}
'@pnpm/exe.linux-arm64@12.5.1':
resolution: {integrity: sha512-6hbMWW2/eQo1b5wvQPe/ndLHVTH88Rav6N+ffD4/h6IuFMjWCgasFYYsFv0GrvpqREAx3SwmZSvli+0NFXqbxA==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@pnpm/exe.linux-x64-musl@12.3.4':
resolution: {integrity: sha512-RPmk7Jb/aYaFvL2iyDN/AtMY+hUEsue732WmXpcuQ9tBpMnGyA5py7Z3+e+qmQaJ0zY/4ni9jJiyPBQHujmv6w==}
'@pnpm/exe.linux-ppc64@12.5.1':
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]
os: [linux]
libc: [musl]
'@pnpm/exe.linux-x64@12.3.4':
resolution: {integrity: sha512-2ZqOlSPkfwX1h5cR+FPiWf8+F+2hZT/3TvhUK5sigHqwaQCIiq8R7CGxhndKs63JtcLi2a1Qpo+wX/EoyfjyJQ==}
'@pnpm/exe.linux-x64@12.5.1':
resolution: {integrity: sha512-3PkUBYo5z4dgtlnTNIFj7QGpcDUAuqXz9WGVigPDCeccEnhGiRkWmA5102TmYJHtwJP3LfmE+ZF9PGeWhn8p9Q==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@pnpm/exe.win32-arm64@12.3.4':
resolution: {integrity: sha512-ANyrHqyqco6SXBysUTRF74itDyyraea7IbFsKFdNXTjcFnfycTDx37EwuhdpPYFNSIh2JhUG4fByclsRfiHX7w==}
'@pnpm/exe.win32-arm64@12.5.1':
resolution: {integrity: sha512-49NvjZIXHly56/PLoEDD7VphbO4N8dwMXoApElDFCpVGDNGeKlwlib1tXtnpTF6i2/XIsAj7GgatHB5KHXgBnA==}
cpu: [arm64]
os: [win32]
'@pnpm/exe.win32-x64@12.3.4':
resolution: {integrity: sha512-WH/KqBPY/hq2Tb7SgQltEZytimcjgKRaCRL/aM9CI0c67iKc5TVmHUhIiL3Ux9FB4bWn36i6XewUcScQI+zG8w==}
'@pnpm/exe.win32-x64@12.5.1':
resolution: {integrity: sha512-ePUmQFPAPJFNMwg1lelDx2PdjHqYRiF0yfg5e6qeDbPNOu97QNo557Ru/SxmCrmt8ydQWNuZXWpdEp019md5vw==}
cpu: [x64]
os: [win32]
pnpm@12.3.4:
resolution: {integrity: sha512-lhqkH7B32joEpEHZ+OFevAyW2o73ELLrZ7+e58sGEOq9SPH9hfUc/+c4RnhfoPh8VqOocqHYk/hEZ0G1zORUVw==}
pnpm@12.5.1:
resolution: {integrity: sha512-4/MFvHhKK8ifWtO2E4iJRw+ujSr182thIW7JHCw9ZAiXdfneOKrDMQROpA8kXLDVZmOS399lgk4ZB+9qLGLeXw==}
engines: {node: '>=18.*'}
hasBin: true
snapshots:
'@pnpm/exe.darwin-arm64@12.3.4':
'@pnpm/exe.android-arm64@12.5.1':
optional: true
'@pnpm/exe.darwin-x64@12.3.4':
'@pnpm/exe.android-x64@12.5.1':
optional: true
'@pnpm/exe.linux-arm64-musl@12.3.4':
'@pnpm/exe.darwin-arm64@12.5.1':
optional: true
'@pnpm/exe.linux-arm64@12.3.4':
'@pnpm/exe.darwin-x64@12.5.1':
optional: true
'@pnpm/exe.linux-x64-musl@12.3.4':
'@pnpm/exe.freebsd-x64@12.5.1':
optional: true
'@pnpm/exe.linux-x64@12.3.4':
'@pnpm/exe.linux-arm64-musl@12.5.1':
optional: true
'@pnpm/exe.win32-arm64@12.3.4':
'@pnpm/exe.linux-arm64@12.5.1':
optional: true
'@pnpm/exe.win32-x64@12.3.4':
'@pnpm/exe.linux-ppc64@12.5.1':
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:
'@pnpm/exe.darwin-arm64': 12.3.4
'@pnpm/exe.darwin-x64': 12.3.4
'@pnpm/exe.linux-arm64': 12.3.4
'@pnpm/exe.linux-arm64-musl': 12.3.4
'@pnpm/exe.linux-x64': 12.3.4
'@pnpm/exe.linux-x64-musl': 12.3.4
'@pnpm/exe.win32-arm64': 12.3.4
'@pnpm/exe.win32-x64': 12.3.4
'@pnpm/exe.android-arm64': 12.5.1
'@pnpm/exe.android-x64': 12.5.1
'@pnpm/exe.darwin-arm64': 12.5.1
'@pnpm/exe.darwin-x64': 12.5.1
'@pnpm/exe.freebsd-x64': 12.5.1
'@pnpm/exe.linux-arm64': 12.5.1
'@pnpm/exe.linux-arm64-musl': 12.5.1
'@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'

View File

@ -5,8 +5,6 @@ set -ex
export CURRENT_VERSION=$1;
export NODE_ENV=production;
corepack enable;
corepack install || exit 1;
pnpm install || exit 1;
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
#/usr/bin/env bash
set -e;
corepack enable;
corepack install;
command -v pnpm >/dev/null || { echo "error: pnpm not found in PATH" >&2; exit 1; };
pnpm install
pnpm exec playwright install chromium;
EOF

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

131
frontend/pnpm-lock.yaml generated
View File

@ -7,96 +7,153 @@ importers:
configDependencies: {}
packageManagerDependencies:
pnpm:
specifier: 12.3.4
version: 12.3.4
specifier: 12.5.1
version: 12.5.1
packages:
'@pnpm/exe.darwin-arm64@12.3.4':
resolution: {integrity: sha512-PAyUol8T1+/+ViOiXAt51ECA+QnfXCqz6foL4bW+LsoX0NcVd5XVEM2mRQu+LV4oc7uRz9zf9U0P+XFfuQeDAw==}
'@pnpm/exe.android-arm64@12.5.1':
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]
os: [darwin]
'@pnpm/exe.darwin-x64@12.3.4':
resolution: {integrity: sha512-fxP9JCk0Cdye+ePuj+GJJLMUMTqHGWRdb1dtv4How876uQ2ehxvenpgiYAir/ceO9PsYUZkFTtyZdx+rRu5QOA==}
'@pnpm/exe.darwin-x64@12.5.1':
resolution: {integrity: sha512-YHwI1g2B+Y2cAEylOR31r781GWijdJL7Gy7TEn/ZpZ5v/Cuwp5/Pz0EFCgo0M2MKLafAbL/laUMGvOpAdnerAw==}
cpu: [x64]
os: [darwin]
'@pnpm/exe.linux-arm64-musl@12.3.4':
resolution: {integrity: sha512-FBOt0/7ye6O6q4AllVV5QMviB6qE6fqkeczV/+MDWQsmo+QJrlfsh6X7CpH/tClVpBZEyIbjpUoT8bNhCYBxEg==}
'@pnpm/exe.freebsd-x64@12.5.1':
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]
os: [linux]
libc: [musl]
'@pnpm/exe.linux-arm64@12.3.4':
resolution: {integrity: sha512-t71AVA7LRqiKTyZ5xMYaZc2n5DfdpMbfokZuiIOXHBOM03ECnF0t4iYwaBDqJgVjlKYUOwaF/bRQajGNA4cJ4w==}
'@pnpm/exe.linux-arm64@12.5.1':
resolution: {integrity: sha512-6hbMWW2/eQo1b5wvQPe/ndLHVTH88Rav6N+ffD4/h6IuFMjWCgasFYYsFv0GrvpqREAx3SwmZSvli+0NFXqbxA==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@pnpm/exe.linux-x64-musl@12.3.4':
resolution: {integrity: sha512-RPmk7Jb/aYaFvL2iyDN/AtMY+hUEsue732WmXpcuQ9tBpMnGyA5py7Z3+e+qmQaJ0zY/4ni9jJiyPBQHujmv6w==}
'@pnpm/exe.linux-ppc64@12.5.1':
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]
os: [linux]
libc: [musl]
'@pnpm/exe.linux-x64@12.3.4':
resolution: {integrity: sha512-2ZqOlSPkfwX1h5cR+FPiWf8+F+2hZT/3TvhUK5sigHqwaQCIiq8R7CGxhndKs63JtcLi2a1Qpo+wX/EoyfjyJQ==}
'@pnpm/exe.linux-x64@12.5.1':
resolution: {integrity: sha512-3PkUBYo5z4dgtlnTNIFj7QGpcDUAuqXz9WGVigPDCeccEnhGiRkWmA5102TmYJHtwJP3LfmE+ZF9PGeWhn8p9Q==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@pnpm/exe.win32-arm64@12.3.4':
resolution: {integrity: sha512-ANyrHqyqco6SXBysUTRF74itDyyraea7IbFsKFdNXTjcFnfycTDx37EwuhdpPYFNSIh2JhUG4fByclsRfiHX7w==}
'@pnpm/exe.win32-arm64@12.5.1':
resolution: {integrity: sha512-49NvjZIXHly56/PLoEDD7VphbO4N8dwMXoApElDFCpVGDNGeKlwlib1tXtnpTF6i2/XIsAj7GgatHB5KHXgBnA==}
cpu: [arm64]
os: [win32]
'@pnpm/exe.win32-x64@12.3.4':
resolution: {integrity: sha512-WH/KqBPY/hq2Tb7SgQltEZytimcjgKRaCRL/aM9CI0c67iKc5TVmHUhIiL3Ux9FB4bWn36i6XewUcScQI+zG8w==}
'@pnpm/exe.win32-x64@12.5.1':
resolution: {integrity: sha512-ePUmQFPAPJFNMwg1lelDx2PdjHqYRiF0yfg5e6qeDbPNOu97QNo557Ru/SxmCrmt8ydQWNuZXWpdEp019md5vw==}
cpu: [x64]
os: [win32]
pnpm@12.3.4:
resolution: {integrity: sha512-lhqkH7B32joEpEHZ+OFevAyW2o73ELLrZ7+e58sGEOq9SPH9hfUc/+c4RnhfoPh8VqOocqHYk/hEZ0G1zORUVw==}
pnpm@12.5.1:
resolution: {integrity: sha512-4/MFvHhKK8ifWtO2E4iJRw+ujSr182thIW7JHCw9ZAiXdfneOKrDMQROpA8kXLDVZmOS399lgk4ZB+9qLGLeXw==}
engines: {node: '>=18.*'}
hasBin: true
snapshots:
'@pnpm/exe.darwin-arm64@12.3.4':
'@pnpm/exe.android-arm64@12.5.1':
optional: true
'@pnpm/exe.darwin-x64@12.3.4':
'@pnpm/exe.android-x64@12.5.1':
optional: true
'@pnpm/exe.linux-arm64-musl@12.3.4':
'@pnpm/exe.darwin-arm64@12.5.1':
optional: true
'@pnpm/exe.linux-arm64@12.3.4':
'@pnpm/exe.darwin-x64@12.5.1':
optional: true
'@pnpm/exe.linux-x64-musl@12.3.4':
'@pnpm/exe.freebsd-x64@12.5.1':
optional: true
'@pnpm/exe.linux-x64@12.3.4':
'@pnpm/exe.linux-arm64-musl@12.5.1':
optional: true
'@pnpm/exe.win32-arm64@12.3.4':
'@pnpm/exe.linux-arm64@12.5.1':
optional: true
'@pnpm/exe.win32-x64@12.3.4':
'@pnpm/exe.linux-ppc64@12.5.1':
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:
'@pnpm/exe.darwin-arm64': 12.3.4
'@pnpm/exe.darwin-x64': 12.3.4
'@pnpm/exe.linux-arm64': 12.3.4
'@pnpm/exe.linux-arm64-musl': 12.3.4
'@pnpm/exe.linux-x64': 12.3.4
'@pnpm/exe.linux-x64-musl': 12.3.4
'@pnpm/exe.win32-arm64': 12.3.4
'@pnpm/exe.win32-x64': 12.3.4
'@pnpm/exe.android-arm64': 12.5.1
'@pnpm/exe.android-x64': 12.5.1
'@pnpm/exe.darwin-arm64': 12.5.1
'@pnpm/exe.darwin-x64': 12.5.1
'@pnpm/exe.freebsd-x64': 12.5.1
'@pnpm/exe.linux-arm64': 12.5.1
'@pnpm/exe.linux-arm64-musl': 12.5.1
'@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'

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -28,5 +28,5 @@
"vite": "^8.2.0",
"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",
"license": "MPL-2.0",
"author": "Kaleidos INC Sucursal en España SL",
"packageManager": "pnpm@12.3.4+sha512.961aa41fb077da3a04a441d9f8e15ebc0c96da8ef710b2eb67bf9ee7cb0610eabd48f1fd85f51cffe73846785fa0f87c56a3a872a1d893f8446741b5cce45457",
"packageManager": "pnpm@12.5.1+sha512.e3f305bc784a2bc89f5ad3b6138889470fae8d2af5f36b61216ec91c2c3d64089775f9de38aac331044ea40f245cb0d5666392dfdf65824e1907ef6a2c62de5f",
"type": "module",
"repository": {
"type": "git",

131
library/pnpm-lock.yaml generated
View File

@ -7,96 +7,153 @@ importers:
configDependencies: {}
packageManagerDependencies:
pnpm:
specifier: 12.3.4
version: 12.3.4
specifier: 12.5.1
version: 12.5.1
packages:
'@pnpm/exe.darwin-arm64@12.3.4':
resolution: {integrity: sha512-PAyUol8T1+/+ViOiXAt51ECA+QnfXCqz6foL4bW+LsoX0NcVd5XVEM2mRQu+LV4oc7uRz9zf9U0P+XFfuQeDAw==}
'@pnpm/exe.android-arm64@12.5.1':
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]
os: [darwin]
'@pnpm/exe.darwin-x64@12.3.4':
resolution: {integrity: sha512-fxP9JCk0Cdye+ePuj+GJJLMUMTqHGWRdb1dtv4How876uQ2ehxvenpgiYAir/ceO9PsYUZkFTtyZdx+rRu5QOA==}
'@pnpm/exe.darwin-x64@12.5.1':
resolution: {integrity: sha512-YHwI1g2B+Y2cAEylOR31r781GWijdJL7Gy7TEn/ZpZ5v/Cuwp5/Pz0EFCgo0M2MKLafAbL/laUMGvOpAdnerAw==}
cpu: [x64]
os: [darwin]
'@pnpm/exe.linux-arm64-musl@12.3.4':
resolution: {integrity: sha512-FBOt0/7ye6O6q4AllVV5QMviB6qE6fqkeczV/+MDWQsmo+QJrlfsh6X7CpH/tClVpBZEyIbjpUoT8bNhCYBxEg==}
'@pnpm/exe.freebsd-x64@12.5.1':
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]
os: [linux]
libc: [musl]
'@pnpm/exe.linux-arm64@12.3.4':
resolution: {integrity: sha512-t71AVA7LRqiKTyZ5xMYaZc2n5DfdpMbfokZuiIOXHBOM03ECnF0t4iYwaBDqJgVjlKYUOwaF/bRQajGNA4cJ4w==}
'@pnpm/exe.linux-arm64@12.5.1':
resolution: {integrity: sha512-6hbMWW2/eQo1b5wvQPe/ndLHVTH88Rav6N+ffD4/h6IuFMjWCgasFYYsFv0GrvpqREAx3SwmZSvli+0NFXqbxA==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@pnpm/exe.linux-x64-musl@12.3.4':
resolution: {integrity: sha512-RPmk7Jb/aYaFvL2iyDN/AtMY+hUEsue732WmXpcuQ9tBpMnGyA5py7Z3+e+qmQaJ0zY/4ni9jJiyPBQHujmv6w==}
'@pnpm/exe.linux-ppc64@12.5.1':
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]
os: [linux]
libc: [musl]
'@pnpm/exe.linux-x64@12.3.4':
resolution: {integrity: sha512-2ZqOlSPkfwX1h5cR+FPiWf8+F+2hZT/3TvhUK5sigHqwaQCIiq8R7CGxhndKs63JtcLi2a1Qpo+wX/EoyfjyJQ==}
'@pnpm/exe.linux-x64@12.5.1':
resolution: {integrity: sha512-3PkUBYo5z4dgtlnTNIFj7QGpcDUAuqXz9WGVigPDCeccEnhGiRkWmA5102TmYJHtwJP3LfmE+ZF9PGeWhn8p9Q==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@pnpm/exe.win32-arm64@12.3.4':
resolution: {integrity: sha512-ANyrHqyqco6SXBysUTRF74itDyyraea7IbFsKFdNXTjcFnfycTDx37EwuhdpPYFNSIh2JhUG4fByclsRfiHX7w==}
'@pnpm/exe.win32-arm64@12.5.1':
resolution: {integrity: sha512-49NvjZIXHly56/PLoEDD7VphbO4N8dwMXoApElDFCpVGDNGeKlwlib1tXtnpTF6i2/XIsAj7GgatHB5KHXgBnA==}
cpu: [arm64]
os: [win32]
'@pnpm/exe.win32-x64@12.3.4':
resolution: {integrity: sha512-WH/KqBPY/hq2Tb7SgQltEZytimcjgKRaCRL/aM9CI0c67iKc5TVmHUhIiL3Ux9FB4bWn36i6XewUcScQI+zG8w==}
'@pnpm/exe.win32-x64@12.5.1':
resolution: {integrity: sha512-ePUmQFPAPJFNMwg1lelDx2PdjHqYRiF0yfg5e6qeDbPNOu97QNo557Ru/SxmCrmt8ydQWNuZXWpdEp019md5vw==}
cpu: [x64]
os: [win32]
pnpm@12.3.4:
resolution: {integrity: sha512-lhqkH7B32joEpEHZ+OFevAyW2o73ELLrZ7+e58sGEOq9SPH9hfUc/+c4RnhfoPh8VqOocqHYk/hEZ0G1zORUVw==}
pnpm@12.5.1:
resolution: {integrity: sha512-4/MFvHhKK8ifWtO2E4iJRw+ujSr182thIW7JHCw9ZAiXdfneOKrDMQROpA8kXLDVZmOS399lgk4ZB+9qLGLeXw==}
engines: {node: '>=18.*'}
hasBin: true
snapshots:
'@pnpm/exe.darwin-arm64@12.3.4':
'@pnpm/exe.android-arm64@12.5.1':
optional: true
'@pnpm/exe.darwin-x64@12.3.4':
'@pnpm/exe.android-x64@12.5.1':
optional: true
'@pnpm/exe.linux-arm64-musl@12.3.4':
'@pnpm/exe.darwin-arm64@12.5.1':
optional: true
'@pnpm/exe.linux-arm64@12.3.4':
'@pnpm/exe.darwin-x64@12.5.1':
optional: true
'@pnpm/exe.linux-x64-musl@12.3.4':
'@pnpm/exe.freebsd-x64@12.5.1':
optional: true
'@pnpm/exe.linux-x64@12.3.4':
'@pnpm/exe.linux-arm64-musl@12.5.1':
optional: true
'@pnpm/exe.win32-arm64@12.3.4':
'@pnpm/exe.linux-arm64@12.5.1':
optional: true
'@pnpm/exe.win32-x64@12.3.4':
'@pnpm/exe.linux-ppc64@12.5.1':
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:
'@pnpm/exe.darwin-arm64': 12.3.4
'@pnpm/exe.darwin-x64': 12.3.4
'@pnpm/exe.linux-arm64': 12.3.4
'@pnpm/exe.linux-arm64-musl': 12.3.4
'@pnpm/exe.linux-x64': 12.3.4
'@pnpm/exe.linux-x64-musl': 12.3.4
'@pnpm/exe.win32-arm64': 12.3.4
'@pnpm/exe.win32-x64': 12.3.4
'@pnpm/exe.android-arm64': 12.5.1
'@pnpm/exe.android-x64': 12.5.1
'@pnpm/exe.darwin-arm64': 12.5.1
'@pnpm/exe.darwin-x64': 12.5.1
'@pnpm/exe.freebsd-x64': 12.5.1
'@pnpm/exe.linux-arm64': 12.5.1
'@pnpm/exe.linux-arm64-musl': 12.5.1
'@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'

View File

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

View File

@ -209,9 +209,7 @@ function ensure-devenv-network {
# Compose-project plumbing for the parallel-workspaces layout.
#
# - Shared infrastructure (postgres, minio, mailer, ldap, minio-setup) runs
# under project `penpotdev-infra`.
# - Shared infrastructure (postgres, minio, mailer, ldap, valkey, minio-setup)
# - Shared infrastructure (postgres, RustFS, mailer, LDAP, Valkey)
# runs under project `penpotdev-infra`.
# - Each runtime instance (ws0, ws1, ...) runs only its own main container
# 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" ]]
}
# Bring shared infra up and block until minio-setup has provisioned the
# shared MinIO user/policy. Idempotent: a second call when everything is
# already up returns immediately.
# Bring shared infra up and block until services with healthchecks are healthy.
# Removing orphaned containers retires old infra services without deleting
# their named volumes.
function ensure-infra-up {
infra-compose up -d
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
infra-compose up -d --wait --wait-timeout 60 --remove-orphans
}
# 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
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.

View File

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

View File

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

View File

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

View File

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

131
mcp/pnpm-lock.yaml generated
View File

@ -7,96 +7,153 @@ importers:
configDependencies: {}
packageManagerDependencies:
pnpm:
specifier: 12.3.4
version: 12.3.4
specifier: 12.5.1
version: 12.5.1
packages:
'@pnpm/exe.darwin-arm64@12.3.4':
resolution: {integrity: sha512-PAyUol8T1+/+ViOiXAt51ECA+QnfXCqz6foL4bW+LsoX0NcVd5XVEM2mRQu+LV4oc7uRz9zf9U0P+XFfuQeDAw==}
'@pnpm/exe.android-arm64@12.5.1':
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]
os: [darwin]
'@pnpm/exe.darwin-x64@12.3.4':
resolution: {integrity: sha512-fxP9JCk0Cdye+ePuj+GJJLMUMTqHGWRdb1dtv4How876uQ2ehxvenpgiYAir/ceO9PsYUZkFTtyZdx+rRu5QOA==}
'@pnpm/exe.darwin-x64@12.5.1':
resolution: {integrity: sha512-YHwI1g2B+Y2cAEylOR31r781GWijdJL7Gy7TEn/ZpZ5v/Cuwp5/Pz0EFCgo0M2MKLafAbL/laUMGvOpAdnerAw==}
cpu: [x64]
os: [darwin]
'@pnpm/exe.linux-arm64-musl@12.3.4':
resolution: {integrity: sha512-FBOt0/7ye6O6q4AllVV5QMviB6qE6fqkeczV/+MDWQsmo+QJrlfsh6X7CpH/tClVpBZEyIbjpUoT8bNhCYBxEg==}
'@pnpm/exe.freebsd-x64@12.5.1':
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]
os: [linux]
libc: [musl]
'@pnpm/exe.linux-arm64@12.3.4':
resolution: {integrity: sha512-t71AVA7LRqiKTyZ5xMYaZc2n5DfdpMbfokZuiIOXHBOM03ECnF0t4iYwaBDqJgVjlKYUOwaF/bRQajGNA4cJ4w==}
'@pnpm/exe.linux-arm64@12.5.1':
resolution: {integrity: sha512-6hbMWW2/eQo1b5wvQPe/ndLHVTH88Rav6N+ffD4/h6IuFMjWCgasFYYsFv0GrvpqREAx3SwmZSvli+0NFXqbxA==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@pnpm/exe.linux-x64-musl@12.3.4':
resolution: {integrity: sha512-RPmk7Jb/aYaFvL2iyDN/AtMY+hUEsue732WmXpcuQ9tBpMnGyA5py7Z3+e+qmQaJ0zY/4ni9jJiyPBQHujmv6w==}
'@pnpm/exe.linux-ppc64@12.5.1':
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]
os: [linux]
libc: [musl]
'@pnpm/exe.linux-x64@12.3.4':
resolution: {integrity: sha512-2ZqOlSPkfwX1h5cR+FPiWf8+F+2hZT/3TvhUK5sigHqwaQCIiq8R7CGxhndKs63JtcLi2a1Qpo+wX/EoyfjyJQ==}
'@pnpm/exe.linux-x64@12.5.1':
resolution: {integrity: sha512-3PkUBYo5z4dgtlnTNIFj7QGpcDUAuqXz9WGVigPDCeccEnhGiRkWmA5102TmYJHtwJP3LfmE+ZF9PGeWhn8p9Q==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@pnpm/exe.win32-arm64@12.3.4':
resolution: {integrity: sha512-ANyrHqyqco6SXBysUTRF74itDyyraea7IbFsKFdNXTjcFnfycTDx37EwuhdpPYFNSIh2JhUG4fByclsRfiHX7w==}
'@pnpm/exe.win32-arm64@12.5.1':
resolution: {integrity: sha512-49NvjZIXHly56/PLoEDD7VphbO4N8dwMXoApElDFCpVGDNGeKlwlib1tXtnpTF6i2/XIsAj7GgatHB5KHXgBnA==}
cpu: [arm64]
os: [win32]
'@pnpm/exe.win32-x64@12.3.4':
resolution: {integrity: sha512-WH/KqBPY/hq2Tb7SgQltEZytimcjgKRaCRL/aM9CI0c67iKc5TVmHUhIiL3Ux9FB4bWn36i6XewUcScQI+zG8w==}
'@pnpm/exe.win32-x64@12.5.1':
resolution: {integrity: sha512-ePUmQFPAPJFNMwg1lelDx2PdjHqYRiF0yfg5e6qeDbPNOu97QNo557Ru/SxmCrmt8ydQWNuZXWpdEp019md5vw==}
cpu: [x64]
os: [win32]
pnpm@12.3.4:
resolution: {integrity: sha512-lhqkH7B32joEpEHZ+OFevAyW2o73ELLrZ7+e58sGEOq9SPH9hfUc/+c4RnhfoPh8VqOocqHYk/hEZ0G1zORUVw==}
pnpm@12.5.1:
resolution: {integrity: sha512-4/MFvHhKK8ifWtO2E4iJRw+ujSr182thIW7JHCw9ZAiXdfneOKrDMQROpA8kXLDVZmOS399lgk4ZB+9qLGLeXw==}
engines: {node: '>=18.*'}
hasBin: true
snapshots:
'@pnpm/exe.darwin-arm64@12.3.4':
'@pnpm/exe.android-arm64@12.5.1':
optional: true
'@pnpm/exe.darwin-x64@12.3.4':
'@pnpm/exe.android-x64@12.5.1':
optional: true
'@pnpm/exe.linux-arm64-musl@12.3.4':
'@pnpm/exe.darwin-arm64@12.5.1':
optional: true
'@pnpm/exe.linux-arm64@12.3.4':
'@pnpm/exe.darwin-x64@12.5.1':
optional: true
'@pnpm/exe.linux-x64-musl@12.3.4':
'@pnpm/exe.freebsd-x64@12.5.1':
optional: true
'@pnpm/exe.linux-x64@12.3.4':
'@pnpm/exe.linux-arm64-musl@12.5.1':
optional: true
'@pnpm/exe.win32-arm64@12.3.4':
'@pnpm/exe.linux-arm64@12.5.1':
optional: true
'@pnpm/exe.win32-x64@12.3.4':
'@pnpm/exe.linux-ppc64@12.5.1':
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:
'@pnpm/exe.darwin-arm64': 12.3.4
'@pnpm/exe.darwin-x64': 12.3.4
'@pnpm/exe.linux-arm64': 12.3.4
'@pnpm/exe.linux-arm64-musl': 12.3.4
'@pnpm/exe.linux-x64': 12.3.4
'@pnpm/exe.linux-x64-musl': 12.3.4
'@pnpm/exe.win32-arm64': 12.3.4
'@pnpm/exe.win32-x64': 12.3.4
'@pnpm/exe.android-arm64': 12.5.1
'@pnpm/exe.android-x64': 12.5.1
'@pnpm/exe.darwin-arm64': 12.5.1
'@pnpm/exe.darwin-x64': 12.5.1
'@pnpm/exe.freebsd-x64': 12.5.1
'@pnpm/exe.linux-arm64': 12.5.1
'@pnpm/exe.linux-arm64-musl': 12.5.1
'@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'

View File

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

View File

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

View File

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

View File

@ -7,96 +7,153 @@ importers:
configDependencies: {}
packageManagerDependencies:
pnpm:
specifier: 12.3.4
version: 12.3.4
specifier: 12.5.1
version: 12.5.1
packages:
'@pnpm/exe.darwin-arm64@12.3.4':
resolution: {integrity: sha512-PAyUol8T1+/+ViOiXAt51ECA+QnfXCqz6foL4bW+LsoX0NcVd5XVEM2mRQu+LV4oc7uRz9zf9U0P+XFfuQeDAw==}
'@pnpm/exe.android-arm64@12.5.1':
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]
os: [darwin]
'@pnpm/exe.darwin-x64@12.3.4':
resolution: {integrity: sha512-fxP9JCk0Cdye+ePuj+GJJLMUMTqHGWRdb1dtv4How876uQ2ehxvenpgiYAir/ceO9PsYUZkFTtyZdx+rRu5QOA==}
'@pnpm/exe.darwin-x64@12.5.1':
resolution: {integrity: sha512-YHwI1g2B+Y2cAEylOR31r781GWijdJL7Gy7TEn/ZpZ5v/Cuwp5/Pz0EFCgo0M2MKLafAbL/laUMGvOpAdnerAw==}
cpu: [x64]
os: [darwin]
'@pnpm/exe.linux-arm64-musl@12.3.4':
resolution: {integrity: sha512-FBOt0/7ye6O6q4AllVV5QMviB6qE6fqkeczV/+MDWQsmo+QJrlfsh6X7CpH/tClVpBZEyIbjpUoT8bNhCYBxEg==}
'@pnpm/exe.freebsd-x64@12.5.1':
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]
os: [linux]
libc: [musl]
'@pnpm/exe.linux-arm64@12.3.4':
resolution: {integrity: sha512-t71AVA7LRqiKTyZ5xMYaZc2n5DfdpMbfokZuiIOXHBOM03ECnF0t4iYwaBDqJgVjlKYUOwaF/bRQajGNA4cJ4w==}
'@pnpm/exe.linux-arm64@12.5.1':
resolution: {integrity: sha512-6hbMWW2/eQo1b5wvQPe/ndLHVTH88Rav6N+ffD4/h6IuFMjWCgasFYYsFv0GrvpqREAx3SwmZSvli+0NFXqbxA==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@pnpm/exe.linux-x64-musl@12.3.4':
resolution: {integrity: sha512-RPmk7Jb/aYaFvL2iyDN/AtMY+hUEsue732WmXpcuQ9tBpMnGyA5py7Z3+e+qmQaJ0zY/4ni9jJiyPBQHujmv6w==}
'@pnpm/exe.linux-ppc64@12.5.1':
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]
os: [linux]
libc: [musl]
'@pnpm/exe.linux-x64@12.3.4':
resolution: {integrity: sha512-2ZqOlSPkfwX1h5cR+FPiWf8+F+2hZT/3TvhUK5sigHqwaQCIiq8R7CGxhndKs63JtcLi2a1Qpo+wX/EoyfjyJQ==}
'@pnpm/exe.linux-x64@12.5.1':
resolution: {integrity: sha512-3PkUBYo5z4dgtlnTNIFj7QGpcDUAuqXz9WGVigPDCeccEnhGiRkWmA5102TmYJHtwJP3LfmE+ZF9PGeWhn8p9Q==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@pnpm/exe.win32-arm64@12.3.4':
resolution: {integrity: sha512-ANyrHqyqco6SXBysUTRF74itDyyraea7IbFsKFdNXTjcFnfycTDx37EwuhdpPYFNSIh2JhUG4fByclsRfiHX7w==}
'@pnpm/exe.win32-arm64@12.5.1':
resolution: {integrity: sha512-49NvjZIXHly56/PLoEDD7VphbO4N8dwMXoApElDFCpVGDNGeKlwlib1tXtnpTF6i2/XIsAj7GgatHB5KHXgBnA==}
cpu: [arm64]
os: [win32]
'@pnpm/exe.win32-x64@12.3.4':
resolution: {integrity: sha512-WH/KqBPY/hq2Tb7SgQltEZytimcjgKRaCRL/aM9CI0c67iKc5TVmHUhIiL3Ux9FB4bWn36i6XewUcScQI+zG8w==}
'@pnpm/exe.win32-x64@12.5.1':
resolution: {integrity: sha512-ePUmQFPAPJFNMwg1lelDx2PdjHqYRiF0yfg5e6qeDbPNOu97QNo557Ru/SxmCrmt8ydQWNuZXWpdEp019md5vw==}
cpu: [x64]
os: [win32]
pnpm@12.3.4:
resolution: {integrity: sha512-lhqkH7B32joEpEHZ+OFevAyW2o73ELLrZ7+e58sGEOq9SPH9hfUc/+c4RnhfoPh8VqOocqHYk/hEZ0G1zORUVw==}
pnpm@12.5.1:
resolution: {integrity: sha512-4/MFvHhKK8ifWtO2E4iJRw+ujSr182thIW7JHCw9ZAiXdfneOKrDMQROpA8kXLDVZmOS399lgk4ZB+9qLGLeXw==}
engines: {node: '>=18.*'}
hasBin: true
snapshots:
'@pnpm/exe.darwin-arm64@12.3.4':
'@pnpm/exe.android-arm64@12.5.1':
optional: true
'@pnpm/exe.darwin-x64@12.3.4':
'@pnpm/exe.android-x64@12.5.1':
optional: true
'@pnpm/exe.linux-arm64-musl@12.3.4':
'@pnpm/exe.darwin-arm64@12.5.1':
optional: true
'@pnpm/exe.linux-arm64@12.3.4':
'@pnpm/exe.darwin-x64@12.5.1':
optional: true
'@pnpm/exe.linux-x64-musl@12.3.4':
'@pnpm/exe.freebsd-x64@12.5.1':
optional: true
'@pnpm/exe.linux-x64@12.3.4':
'@pnpm/exe.linux-arm64-musl@12.5.1':
optional: true
'@pnpm/exe.win32-arm64@12.3.4':
'@pnpm/exe.linux-arm64@12.5.1':
optional: true
'@pnpm/exe.win32-x64@12.3.4':
'@pnpm/exe.linux-ppc64@12.5.1':
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:
'@pnpm/exe.darwin-arm64': 12.3.4
'@pnpm/exe.darwin-x64': 12.3.4
'@pnpm/exe.linux-arm64': 12.3.4
'@pnpm/exe.linux-arm64-musl': 12.3.4
'@pnpm/exe.linux-x64': 12.3.4
'@pnpm/exe.linux-x64-musl': 12.3.4
'@pnpm/exe.win32-arm64': 12.3.4
'@pnpm/exe.win32-x64': 12.3.4
'@pnpm/exe.android-arm64': 12.5.1
'@pnpm/exe.android-x64': 12.5.1
'@pnpm/exe.darwin-arm64': 12.5.1
'@pnpm/exe.darwin-x64': 12.5.1
'@pnpm/exe.freebsd-x64': 12.5.1
'@pnpm/exe.linux-arm64': 12.5.1
'@pnpm/exe.linux-arm64-musl': 12.5.1
'@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'

View File

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

View File

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

View File

@ -13,5 +13,5 @@
"lint": "eslint .",
"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
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),
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
@ -182,9 +184,10 @@ backend's only role is persistence, which the mock answers with a canned
response.
Prerequisites: the frontend bundle must exist at `frontend/resources/public`
(the devenv watch build suffices; CI builds it via `frontend/scripts/build`),
and the Playwright browser must be installed
(the devenv watch build suffices), and the Playwright browser must be installed
(`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:

View File

@ -1,8 +1,8 @@
import { spawn, type ChildProcess } from "node:child_process";
import { readFileSync } from "node:fs";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
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,
// 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
// file. If workspace loading changes and this driver times out waiting for the
// 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),
// injects the prebuilt `headless.js` bundle into the plugin sandbox via
// `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
const repoRoot = resolve(here, "../../../../");
const frontendDir = resolve(repoRoot, "frontend");
const staticRoot = resolve(frontendDir, "resources/public");
const e2eDataDir = resolve(frontendDir, "playwright/data");
const BASE_URL = "http://localhost:3000";
@ -96,14 +98,15 @@ async function waitForServer(url: string, timeoutMs = 30000): Promise<void> {
}
}
function startE2eServer(): ChildProcess {
// Reuse the frontend e2e static server: it serves frontend/resources/public
// on port 3000, which is also the host the app opens its notifications
function startE2eServer(): Promise<StaticServer> {
// Serve the prebuilt frontend bundle from `frontend/resources/public` on
// port 3000, which is also the host the app opens its notifications
// WebSocket against — so the WS mock below matches without extra config.
return spawn("node", ["scripts/e2e-server.js"], {
cwd: frontendDir,
stdio: "inherit",
});
// This used to shell out to the express-based
// `frontend/scripts/e2e-server.js`, but that resolves `express` from
// `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
@ -204,7 +207,7 @@ function printReport(results: ReportedResult[]) {
async function main() {
const bundle = readFileSync(headlessBundlePath, "utf-8");
const server = startE2eServer();
const server = await startE2eServer();
await waitForServer(BASE_URL);
const browser = await chromium.launch();
@ -280,7 +283,7 @@ async function main() {
]);
await browser.close();
server.kill();
await server.close();
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:headless": "vite build --config vite.config.headless.ts",
"test:ci": "pnpm run build:headless && tsx ci/run-ci.ts",
"test:unit": "tsx --test ci/static-server.test.ts",
"preview": "vite preview",
"bootstrap": "pnpm install --ignore-workspace && pnpm run build && pnpm run start",
"types:check": "tsc --noEmit",
@ -26,5 +27,5 @@
"vite": "^8.2.2",
"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 .",
"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