🎉 Enable render switch and wasm info by default and simplify feature helpers to use pre-computed features set (#9942)

The setup-wasm-features function is the single source of truth for
    resolving the renderer choice (URL param > profile preference > team
    flags), storing the result in state[:features]. Several helpers were
    re-deriving the same priority chain independently, duplicating logic:

    - wasm-enabled?, wasm-url-override, wasm-url-override-ref
    - enabled-by-flags?, enabled-without-migration?

    This change removes all duplicated helpers and simplifies the
    remaining functions to rely exclusively on the pre-computed
    :features set:

    - active-feature? — now just checks (contains? (:features state)
      feature) without special-casing render-wasm/v1
    - use-feature — uses the reactive features-ref for all features
    - initialize/recompute-features effects — use the local features
      binding directly

    Since :features is rebuilt by setup-wasm-features on every
    initialization and recompute, this preserves correctness while
    eliminating ~50 lines of duplicated code.
This commit is contained in:
Alejandro Alonso 2026-06-01 12:52:34 +02:00 committed by GitHub
parent 2410bcb0df
commit 88f2366c6f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 108 additions and 96 deletions

View File

@ -193,7 +193,9 @@
:enable-inspect-styles
:enable-feature-fdata-objects-map
:enable-feature-render-wasm
:enable-token-import-from-library])
:enable-token-import-from-library
:enable-render-switch
:enable-render-wasm-info])
(defn parse
[& flags]

View File

@ -0,0 +1,24 @@
{
"~:email": "foo@example.com",
"~:is-demo": false,
"~:auth-backend": "penpot",
"~:fullname": "Princesa Leia",
"~:modified-at": "~m1713533116365",
"~:is-active": true,
"~:default-project-id": "~uc7ce0794-0992-8105-8004-38e630f7920b",
"~:id": "~uc7ce0794-0992-8105-8004-38e630f29a9b",
"~:is-muted": false,
"~:default-team-id": "~uc7ce0794-0992-8105-8004-38e630f40f6d",
"~:created-at": "~m1713533116365",
"~:is-blocked": false,
"~:props": {
"~:nudge": {
"~:big": 10,
"~:small": 1
},
"~:v2-info-shown": true,
"~:viewed-tutorial?": false,
"~:viewed-walkthrough?": false,
"~:renderer": "~:wasm"
}
}

View File

@ -1,16 +1,21 @@
import { expect } from "@playwright/test";
import { WorkspacePage } from "./WorkspacePage";
export const WASM_PROFILE = "logged-in-user/get-profile-wasm-renderer.json";
export const WASM_FLAGS = [
"enable-feature-render-wasm",
"enable-render-wasm-dpr",
"enable-feature-text-editor-v2",
// Default flags enable render-wasm-info; keep screenshots stable in e2e.
"disable-render-wasm-info",
];
export class WasmWorkspacePage extends WorkspacePage {
static async init(page) {
await super.init(page);
await WasmWorkspacePage.mockConfigFlags(page, WASM_FLAGS);
await WasmWorkspacePage.mockRPC(page, "get-profile", WASM_PROFILE);
await page.addInitScript(() => {
document.addEventListener("penpot:wasm:loaded", () => {
@ -99,4 +104,9 @@ export class WasmWorkspacePage extends WorkspacePage {
options,
);
}
async setupEmptyFile() {
await super.setupEmptyFile();
await this.mockRPC("get-profile", WASM_PROFILE);
}
}

View File

@ -28,19 +28,42 @@
(defn setup-wasm-features
[features state]
(let [params (rt/get-params state)
wasm (get params :wasm)
renderer (when (contains? cf/flags :render-switch)
(-> state :profile :props :renderer))
enable-wasm (or (= "true" wasm) (and (= renderer :wasm) (not= "false" wasm)))
disable-wasm (or (= "false" wasm) (and (= renderer :svg) (not= "true" wasm)))
features (cond-> features
enable-wasm (conj "render-wasm/v1")
disable-wasm (disj "render-wasm/v1"))]
;; If wasm render is enabled text-editor/v2 must be used
(cond-> features
(contains? features "render-wasm/v1")
(conj "text-editor/v2"))))
(let [wasm-override (-> (rt/get-params state)
(get :wasm))
;; When the :render-switch feature flag is active, read the user's
;; preferred renderer from their profile settings (default :svg).
renderer (when (contains? cf/flags :render-switch)
(dm/get-in state [:profile :props :renderer] :svg))
features (cond
;; Priority 1 — URL query param (?wasm=true|false)
;; overrides everything: user profile and team flags.
(= "true" wasm-override)
(conj features "render-wasm/v1")
(= "false" wasm-override)
(disj features "render-wasm/v1")
;; Priority 2 — User profile preference.
;; If renderer is non-nil, the :render-switch flag is
;; active and profile data has loaded. Respect the
;; user's saved choice (:wasm or :svg).
(some? renderer)
(if (= :wasm renderer)
(conj features "render-wasm/v1")
(disj features "render-wasm/v1"))
;; Priority 3 — Fall back to the team-level
;; feature set unchanged (no override).
:else
features)]
;; The WASM renderer requires the v2 text editor (hard dependency).
;; Ensure it's always enabled whenever render-wasm/v1 is active.
(if (contains? features "render-wasm/v1")
(conj features "text-editor/v2")
(disj features "text-editor/v2"))))
(defn get-enabled-features
"An explicit lookup of enabled features for the current team"
@ -52,65 +75,15 @@
(set/union (get team :features))
(setup-wasm-features state))))
(defn enabled-by-flags?
[{:keys [features-runtime features]} feature]
(or (contains? features-runtime feature)
(contains? features feature)))
(defn enabled-without-migration?
[{:keys [features-runtime features]} feature]
(or (contains? features-runtime feature)
(contains? global-enabled-features feature)
(contains? features feature)))
(defn wasm-url-override
[state]
(case (get (rt/get-params state) :wasm)
"true" true
"false" false
nil))
(def wasm-url-override-ref
(l/derived wasm-url-override st/state))
(defn- wasm-enabled?
[state]
(let [override (wasm-url-override state)
renderer (when (contains? cf/flags :render-switch)
(-> state :profile :props :renderer))]
(cond
(some? override)
override
(contains? cf/flags :render-switch)
(case renderer
:wasm true
:svg false
;; SVG renderer as default until profile data arrives OR if render-switch
;; flag is disabled.
false)
(contains? cfeat/no-migration-features "render-wasm/v1")
(enabled-without-migration? state "render-wasm/v1")
:else
(enabled-by-flags? state "render-wasm/v1"))))
(defn active-feature?
"Given a state and feature, check if feature is enabled."
"Given a state and feature, check if feature is enabled.
Relies on the pre-computed :features set in state, which already
incorporates URL overrides, user profile preferences, team flags,
and runtime toggles via setup-wasm-features."
[state feature]
(assert (contains? cfeat/supported-features feature)
"feature not supported")
(cond
(= feature "render-wasm/v1")
(wasm-enabled? state)
(contains? cfeat/no-migration-features feature)
(enabled-without-migration? state feature)
:else
(enabled-by-flags? state feature)))
(contains? (:features state) feature))
(defn active-features?
"Given a state and a set of features, check if the features are all enabled."
@ -134,26 +107,12 @@
(l/derived (l/key :features) st/state))
(defn use-feature
"A react hook that checks if feature is currently enabled"
"A react hook that checks if feature is currently enabled.
Uses the pre-computed :features set from state — the same set that
setup-wasm-features has already resolved."
[feature]
(let [enabled-features (mf/deref features-ref)
wasm-override (mf/deref wasm-url-override-ref)
renderer (mf/deref (l/derived #(-> % :profile :props :renderer) st/state))
wasm-enabled (cond
(some? wasm-override)
wasm-override
(contains? cf/flags :render-switch)
(= renderer :wasm)
:else
(contains? enabled-features "render-wasm/v1"))]
(cond
(= feature "render-wasm/v1")
wasm-enabled
:else
(contains? enabled-features feature))))
(let [enabled-features (mf/deref features-ref)]
(contains? enabled-features feature)))
(defn toggle-feature
"An event constructor for runtime feature toggle.
@ -204,7 +163,7 @@
ptk/EffectEvent
(effect [_ state _]
(let [features (get state :features)]
(if (active-feature? state "render-wasm/v1")
(if (contains? features "render-wasm/v1")
(wasm/initialize true)
(wasm/initialize false))
@ -225,7 +184,7 @@
ptk/EffectEvent
(effect [_ state _]
(let [features (get state :features)]
(if (active-feature? state "render-wasm/v1")
(if (contains? features "render-wasm/v1")
(wasm/initialize true)
(wasm/initialize false))

View File

@ -12,6 +12,7 @@
[app.config :as cf]
[app.main.data.team :as dtm]
[app.main.errors :as errors]
[app.main.features :as features]
[app.main.repo :as rp]
[app.main.router :as rt]
[app.main.store :as st]
@ -147,6 +148,14 @@
[]
(ptk/reify ::init-routes
ptk/WatchEvent
(watch [_ _ _]
(rx/of (rt/initialize-router routes)
(rt/initialize-history on-navigate)))))
(watch [_ _ stream]
(rx/merge
(rx/of (rt/initialize-router routes)
(rt/initialize-history on-navigate))
(->> stream
(rx/filter (ptk/type? ::rt/navigated))
(rx/map deref)
(rx/map #(dm/get-in % [:query-params :wasm]))
(rx/buffer 2 1)
(rx/filter (fn [[v1 v2]] (not= v1 v2)))
(rx/map features/recompute-features))))))

View File

@ -10,6 +10,7 @@
[app.common.schema :as sm]
[app.common.test-helpers.files :as cthf]
[app.main.data.workspace.layout :as layout]
[app.main.features :as features]
[beicon.v2.core :as rx]
[potok.v2.core :as ptk]))
@ -18,7 +19,10 @@
:workspace-global layout/default-global
:current-file-id nil
:current-page-id nil
:features-team #{"components/v2"}})
:features-team #{"components/v2"}
;; With :render-switch enabled by default, render-wasm/v1 follows the
;; profile renderer preference instead of the global feature flag alone.
:profile {:props {:renderer :wasm}}})
(defn- on-error
[cause]
@ -34,6 +38,9 @@
:permissions {:can-edit true}
:files {(:id file) file}))
store (ptk/store {:state state :on-error on-error})]
;; Unit tests skip team/workspace bootstrap; mirror team init so
;; :features is populated the same way as features/initialize does in app.
(ptk/emit! store (features/initialize #{}))
store))
(defn run-store

View File

@ -8,8 +8,9 @@
"Test helpers for mocking WASM API boundary functions.
In the Node.js test environment the WASM binary is not available,
but the `render-wasm/v1` feature flag is enabled by default, so
every geometry-modifying event takes the WASM code path.
but `render-wasm/v1` is active when the test store uses a profile
with `:renderer :wasm` (see `frontend-tests.helpers.state`), so
geometry/text events take the WASM code path.
This namespace provides lightweight mock implementations that let
the Clojure-side logic execute normally while stubbing out every
call that would touch the WASM heap.