penpot/frontend/shadow-cljs.edn
Álvaro Tejero Cantero 75c96c32cc
🐛 Name datascript's externs in the builds that rename properties
datascript.query labels a datom's slots with the string names "e", "a",
"v" and "tx", and reads one back with a bracket access on the datom
object. Closure renames the field and leaves the string, so the read
succeeds and returns whichever field inherited the old name. The index
reads the same slots as real fields, so storage keeps answering
correctly and the corruption stays silent.

Datascript ships datascript/externs.js to pin those five names and
declares it in its deps.cljs. shadow-cljs collects that declaration
into a :deps-externs entry it never populates, and reads it only behind
an unset :cljsjs-externs flag, so the declaration is inert and the
:worker release names the file itself. It costs 170 B on the probe
build's 436 197 B.

bench.debug becomes the gate rather than a reproducer: six readings,
one per datom slot plus a join and an index control, each paired with
the answer it must give, exiting non-zero on a wrong one. The new
ds-probe build compiles it with the worker's release options. Without
the externs entry that build reads nil for every transaction, and a
vanilla shadow project loses the value slot as well, which is why the
gate binds each slot on its own.

The ab-bench pin stays at :simple, and its comment now names the
reason that still holds: one compiler configuration across both
engines, matching the 2026-08-21 V8 table.

Answers mirror:ds-advanced-corruption.

AI-assisted-by: opus-5
2026-09-03 07:48:03 +02:00

226 lines
7.1 KiB
Clojure

{:deps {:aliases [:dev]}
:http {:port #shadow/env ["HTTP_PORT" :as :int :default 3448]}
:nrepl {:port #shadow/env ["NREPL_PORT" :as :int :default 3447] :host "0.0.0.0"}
:dev-http {#shadow/env ["DEV_PORT" :as :int :default 8888] "classpath:public"}
:cache-dir #shadow/env ["CACHE" :default ".shadow-cljs"]
:builds
{:main
{:target :esm
:output-dir "resources/public/js/"
:asset-path "/js"
:devtools {:watch-dir "resources/public"
:reload-strategy :full}
:dev {;; allows remote-relay per parallel environment
;; inside :dev so the integration tests won't use it
:devtools {:devtools-url #shadow/env ["SHADOW_SERVER_URL" :default ""]}}
:build-options {:manifest-name "manifest.json"}
:modules
{:shared
{:entries []}
:main
{:entries [app.main app.plugins.api]
:depends-on #{:shared}
:exports {init app.main/init}}
:util-highlight
{:entries [app.util.code-highlight]
:depends-on #{:shared}}
:main-auth
{:entries [app.main.ui.auth
app.main.ui.auth.verify-token]
:depends-on #{:shared}}
:main-viewer
{:entries [app.main.ui.viewer]
:depends-on #{:shared :main-auth}}
:main-workspace
{:entries [app.main.ui.workspace]
:depends-on #{:shared}}
:main-dashboard
{:entries [app.main.ui.dashboard]
:depends-on #{:shared}}
:main-settings
{:entries [app.main.ui.settings]
:depends-on #{:shared}}
:render
{:entries [app.render]
:depends-on #{:shared}
:exports {init app.render/init}}
:rasterizer
{:entries [app.rasterizer]
:depends-on #{:shared}
:exports {init app.rasterizer/init}}}
:js-options
{:entry-keys ["module" "browser" "main"]
:export-conditions ["module" "import", "browser" "require" "default"]
:js-provider :external
:external-index "target/index.js"
:external-index-format :esm}
:compiler-options
{:output-feature-set :es2020
:output-wrapper false
:warnings {:fn-deprecated false}}
:release
{:closure-defines {goog.DEBUG false
goog.debug.LOGGING_ENABLED true
app.config/compiled-version-tag #shadow/env ["VERSION_TAG" :default "develop"]}
:compiler-options
{:fn-invoke-direct true
:optimizations #shadow/env ["PENPOT_BUILD_OPTIMIZATIONS" :as :keyword :default :advanced]
:source-map true
:pseudo-names true
:elide-asserts true
:anon-fn-naming-policy :off
:cross-chunk-method-motion false
:source-map-detail-level :all}}}
:worker
{:target :browser
:output-dir "resources/public/js/worker/"
:asset-path "/js/worker"
:devtools {:watch-dir "resources/public"
:reload-strategy :full
:browser-inject :main}
:dev {;; allows remote-relay per parallel environment
;; inside :dev so the integration tests won't use it
:devtools {:devtools-url #shadow/env ["SHADOW_SERVER_URL" :default ""]}}
:build-options {:manifest-name "manifest.json"}
:modules
{:main
{:entries [app.worker]
:web-worker true
:prepend-js "importScripts('./render.js');"
:depends-on #{}}}
:js-options
{:entry-keys ["module" "browser" "main"]
:export-conditions ["module" "import", "browser" "require" "default"]}
:compiler-options
{:output-feature-set :es2020
:output-wrapper false
:warnings {:fn-deprecated false}}
:release
{:compiler-options
{:fn-invoke-direct true
:optimizations #shadow/env ["PENPOT_BUILD_OPTIMIZATIONS" :as :keyword :default :advanced]
:output-wrapper true
:rename-prefix-namespace "PENPOT"
:source-map true
:elide-asserts true
:anon-fn-naming-policy :off
;; datascript's query layer reads a datom's slots by string property
;; name, so property renaming silently returns the wrong slot: see
;; bench.debug, the gate that proves it, and mirror:ds-advanced-corruption.
;; The library ships these externs and declares them in its deps.cljs,
;; which shadow-cljs does not read, so the build names them here.
:externs ["datascript/externs.js"]
:source-map-detail-level :all}}}
;; FIXME: maybe rename to :components ? (there are nothing storybook
;; related, is just an ESM export of components that will be used
;; initially on storybook but not limited to storybook)
:storybook
{:target :esm
:output-dir "target/storybook/"
:devtools {:enabled false
:console-support false}
:js-options
{:js-provider :import
:entry-keys ["module" "browser" "main"]
:export-conditions ["module" "import", "browser" "require" "default"]}
:modules
{:components
{:exports {default app.main.ui.ds/default
helpers app.main.ui.ds.helpers/default}
:prepend-js ";(globalThis.goog.provide = globalThis.goog.constructNamespace_);(globalThis.goog.require = globalThis.goog.module.get);"
:depends-on #{}}}
:compiler-options
{:output-feature-set :es-next
:output-wrapper false
:warnings {:fn-deprecated false}}}
:test
{:target :esm
:output-dir "target/tests"
:runtime :custom
:js-options {:js-provider :import}
:modules
{:test {:init-fn frontend-tests.runner/init
:prepend-js ";if (typeof globalThis.navigator?.userAgent === 'undefined') { globalThis.navigator = {userAgent: ''}; };"}}}
;; The A/B bench pins :simple so the two engines are measured under one
;; compiler configuration; the 2026-08-21 datahike report's V8 table was
;; read that way. Lifting the pin needs :externs ["datascript/externs.js"]
;; here, for the reason bench.debug states, and a fresh table.
:ab-bench
{:target :node-script
:output-to "target/ab-bench.js"
:output-dir "target/ab-bench/"
:main bench.core/-main
:compiler-options
{:optimizations :simple
:output-wrapper false
:warnings {:fn-deprecated false}}}
;; The gate for mirror:ds-advanced-corruption: bench.debug reads one
;; database six ways and exits non-zero on a wrong answer. Its release
;; options mirror :worker's, because the worker is the build the answer is
;; about. To see the corruption, delete the :externs entry below and
;; rebuild: `--config-merge` reaches a build's own keys and never a
;; :release profile's, and externs from several places are concatenated, so
;; no command line can take this one away.
:ds-probe
{:target :node-script
:output-to "target/ds-probe.js"
:output-dir "target/ds-probe/"
:main bench.debug/-main
:compiler-options
{:output-feature-set :es2020
:output-wrapper false
:warnings {:fn-deprecated false}}
:release
{:compiler-options
{:fn-invoke-direct true
:optimizations #shadow/env ["PENPOT_BUILD_OPTIMIZATIONS" :as :keyword :default :advanced]
:elide-asserts true
:anon-fn-naming-policy :off
:externs ["datascript/externs.js"]}}}
:bench
{:target :node-script
:output-to "target/bench.js"
:output-dir "target/bench/"
:main bench/main
:compiler-options
{:output-feature-set :es2020
:output-wrapper false
:warnings {:fn-deprecated false}}
:release
{:compiler-options
{:fn-invoke-direct true
:elide-asserts true
:anon-fn-naming-policy :off}}}}}