Two problems made this fork expensive to maintain: scanner noise nobody could act on, and divergence from upstream that nothing tracked. Code scanning reported 3,518 open alerts. 3,449 were fallow findings and 2,399 of those sat in vendored bundles `.fallow.toml` never listed (jolt-physics.wasm.js 436, box2djs/box2d.js 328, the Firebase SDK 655, Recast, PeerJS, draco). Listing them plus raising the CRAP thresholds from the defaults to evidence-based values takes fallow from 3,449 findings to 155, all in maintained code. The remaining 69 were CodeQL: 8 third-party actions are now pinned to full commit SHAs with `# vN` comments (so Dependabot keeps updating them) and all 10 workflows declare an explicit least-privilege `permissions:` block. SonarCloud's 2,845 open issues are mostly not this fork's code — 2,526 sit in files byte-identical to upstream. The real defect was configuration: ~200 lines of `sonar.issue.ignore.multicriteria.e1..e88` in .sonarcloud.properties, plus scripts/sonarcloud-apply-suppressions.sh, were inert. That key is a Project Settings UI setting and cannot be read from that file under automatic analysis, so two phases of remediation work chased config the server never saw. Both are deleted; the exclusions that do work are kept and extended. `sonar.test.inclusions` no longer declares production source directories as test inclusions. Divergence is now bounded and checked. `fork-divergence.json` is a checked-in allowlist of the 111 modified, 51 fork-only and 7 upstream-missing paths, and `scripts/check-fork-divergence.js` fails CI on divergence that is not listed or on stale entries. It reads upstream through the GitHub trees API because 4ian/GDevelop is ~1.7GB and this runs on every PR. Restoring Extensions/Physics3DBehavior/tests/ Physics3DRuntimeBehavior.spec.js — deleted by an earlier SonarCloud pass to silence one rule — removes one entry each. Repairs found along the way: every `// nosonar` marker sat on the line before the statement SonarCloud flags, where the directive does nothing; each is now on the exact reported line. Four garbled comment fragments left in multiplayertools.ts by the same pass are gone. A two-line re-export shim with no importers (AiGeneration/CustomAIClient.js) is deleted. `scripts/sonarcloud-drift.js` now writes its outputs to $GITHUB_OUTPUT and seeds its own baseline, so the weekly drift job's ceiling can actually fire; it previously ran behind `continue-on-error` against a threshold that predated the exclusions and was unreachable. Root documentation collapses to MAINTENANCE.md. The three SonarCloud phase plans and the Dependabot plan described steps that were proven impossible and are deleted; AI_GAME_STUDIO_ASSESSMENT.md is a design record and stays. Verified: newIDE/app 176/176 suites and 1,922 tests pass; GDevelop.js 33/33 suites and 1,177 tests pass; GDJS Karma 1,287 pass with the same 9 pre-existing failures (Firebase network tests) reproduced on a pristine tree. check-format lists 37 files against the documented 41-file baseline, so no new violations. Lockfile, drift and divergence guards all exit 0.
26 KiB
Repository Guidelines
GDevelop (BYOK Edition) — a fork of 4ian/GDevelop: a no-code 2D/3D game engine. Games are built from events that are transpiled to JavaScript at export time; the editor is a React/Electron app that manipulates the project through C++ compiled to WebAssembly.
Four language areas, four toolchains. Identify which area you are editing before running anything.
Project Overview
| Area | Language | What it is |
|---|---|---|
Core/ |
C++11 (GDCore) |
The game data model: gd::Project, gd::Layout, objects, behaviors, events, extension metadata. Editor-side only. |
GDJS/ |
TypeScript + C++ | GDJS/Runtime = the game engine shipped in every exported game. GDJS/GDJS = the C++ IDE-side platform (exporter, event→JS codegen). |
GDevelop.js/ |
C++/Emscripten + JS | WebIDL bindings compiling Core + GDJS/GDJS + the C++ extensions into libGD.js + libGD.wasm. Editor-only. |
newIDE/ |
JavaScript (Flow) | The React editor (app/), Electron shell (electron-app/), deploy scripts (web-app/), Puppeteer monkey tests (visual-tests/). |
Extensions/ |
TS + JS + C++ | 48 built-in extensions: IDE declaration + runtime implementation. 31 declare via JsExtension.js, 17 via C++. |
Fork-specific additions on top of upstream: BYOK/local-AI endpoint support, client-side feature unlocks (no watermark, no splash clamp, network preview, debugger, profile), and a Heretek release/update feed. See Fork Divergence — these are the recurring upstream-sync conflict surface.
Architecture & Data Flow
graph TD
CORE["Core/GDCore (C++)<br/>Project / Layout / Object / Events"] --> BIND["GDevelop.js"]
GDJSC["GDJS/GDJS (C++)<br/>Exporter + EventsCodeGenerator"] --> BIND
EXT["Extensions (JsExtension.js + .ts)<br/>declaration + runtime"] --> BIND
BIND -->|Emscripten WebIDL| LIBGD["libGD.js + libGD.wasm"]
LIBGD --> IDE["newIDE/app (React, Flow)"]
EXT --> ESBUILD["GDJS/scripts/build.js (esbuild)"]
GDJS_RT["GDJS/Runtime (TS, gdjs namespace)"] --> ESBUILD
ESBUILD --> RUNTIME["newIDE/app/resources/GDJS/Runtime"]
IDE -->|preview / export| RUNTIME
IDE --> ELECTRON["newIDE/electron-app (electron-builder)"]
Two halves, always. "IDE" = editor-time code (GDCore, GDJS/GDJS, libGD.js); "Runtime" = in-game
code (GDJS/Runtime, extension .ts). They share no classes: gd::Variable (C++, serialized into the
project file) is unrelated to gdjs.Variable (JS, alive during play). A change to one usually needs a
matching change in the other plus a serialization round-trip. See Core/GDevelop-Architecture-Overview.md.
Events are transpiled, not interpreted. There is no runtime event object. gd::Instruction (a
condition/action name + parameters) is turned into JavaScript by GDJS/GDJS/Events/CodeGeneration/EventsCodeGenerator.h.
Generated-file boundary — never hand-edit:
| Generated artifact | Producer |
|---|---|
GDevelop.js/Bindings/glue.cpp, glue.js |
GDevelop.js/update-bindings.js (webidl_binder on Bindings.idl) |
GDevelop.js/Bindings/Wrapper.cpp, postjs.js |
listed as generated in .fallow.toml |
GDevelop.js/types.d.ts, GDevelop.js/types/** |
scripts/generate-dts.mjs, scripts/generate-types.js |
Core/GDCore/Tools/VersionPriv.h |
GDevelop.js/scripts/sync-versions.js |
Binaries/**, newIDE/app/public/libGD.{js,wasm} |
Emscripten build, scripts/copy-to-newIDE.js |
newIDE/app/resources/GDJS/** |
esbuild via newIDE/app/scripts/import-GDJS-Runtime.js |
newIDE/app/src/Version/VersionMetadata.js, public/service-worker.js |
scripts/make-version-metadata.js, make-service-worker.js |
newIDE/app/src/locales/** (messages.js, LocalesMetadata.js) |
compile-translations.js |
newIDE/app/src/UI/Theme/**/*ThemeVariables.* |
build-theme-resources.js |
Extensions/{Physics3DBehavior,TileMap,NavMeshPathfinding}/JsExtension.js |
frozen codemod output (.fallow.toml) |
Vendored (checked in, do not lint or "fix"): GDJS/Runtime/libs/**, GDJS/Runtime/pixi-renderers/{pixi,three,ThreeAddons}.js,
Extensions/Spine/spine-pixi-v7/**, Extensions/Firebase/B_firebasetools/**,
Extensions/{Physics2Behavior/box2d.js,PhysicsBehavior/{Box2D,box2djs},TileMap/{pako,pixi-tilemap}}/**,
Extensions/ParticleSystem/SPARK/**, newIDE/app/public/external/**. .gitattributes marks most of these
linguist-vendored; the authoritative analysis exclusion list is ignorePatterns in .fallow.toml
(mirrored in .sonarcloud.properties).
Key Directories
| Path | Purpose |
|---|---|
Core/GDCore/Project/ |
Serialized game structure: Project.h, Layout.h, Object.h, EventsFunctionsExtension.h |
Core/GDCore/Events/ |
Event types (Builtin/), Parsers/ExpressionParser2.h, abstract CodeGeneration/EventsCodeGenerator.h |
Core/GDCore/Extensions/ |
PlatformExtension.h (the extension declaration API) + Metadata/ + Builtin/ |
Core/GDCore/IDE/ |
Editor-only tools — WholeProjectRefactorer.h (project-wide rename/delete), visitor trees |
Core/tests/ |
Catch v1 tests; DummyPlatform.h is the shared fixture |
GDJS/Runtime/ |
Game engine. gd.ts (namespace + registries), runtimegame.ts, runtimescene.ts, runtimeobject.ts, runtimebehavior.ts |
GDJS/GDJS/Events/CodeGeneration/ |
The events→JS transpiler |
GDJS/GDJS/IDE/ |
Exporter.h, ExporterHelper.cpp (authoritative ordered runtime script list) |
GDevelop.js/Bindings/Bindings.idl |
The WebIDL contract — the 90% file when exposing C++ to JS |
Extensions/<Name>/ |
JsExtension.js (declaration) + *runtimeobject.ts / *runtimebehavior.ts / *tools.ts + tests/ |
newIDE/app/src/ |
Editor. Key: MainFrame/, EventsSheet/, InstancesEditor/, ProjectManager/, ProjectsStorage/, AiGeneration/, Utils/GDevelopServices/ |
newIDE/app/scripts/ |
Build glue: import-GDJS-Runtime.js, import-libGD.js, translation + theme scripts |
newIDE/electron-app/app/main.js |
Electron main process (windows, updater, CLI) |
SharedLibs/{TileMapHelper,ThreeAddons}/ |
Small npm packages bundled separately (rollup) |
Development Commands
No root package.json, no workspaces. 10 independent npm packages, each with its own package-lock.json.
npm install in newIDE/app cascades: its postinstall installs GDJS, then runs import-resources
(which downloads or copies libGD.js and builds the JS runtime). Install order is enforced by those hooks.
Fast path — web dev server
cd newIDE/app
npm install # installs GDJS + import-resources (downloads prebuilt libGD.js if not built locally)
npm start # editor on :3000, GDJS runtime watcher/served on :5002
Desktop app (development)
# terminal 1 — must stay running
cd newIDE/app && npm start
# terminal 2
cd newIDE/electron-app && npm install && npm start
Rebuilding the C++ core (libGD.js) — only when C++ or bindings change
cd GDevelop.js
npm install
git clone https://github.com/juj/emsdk.git ../emsdk && cd ../emsdk
./emsdk install 3.1.21 && ./emsdk activate 3.1.21 && cd ../GDevelop.js
source ../emsdk/emsdk_env.sh
npm run build # grunt: cmake -> emmake make -> copy into newIDE
npm run build -- --variant=dev # faster link (-O0) for iteration
Variants: release (default), dev, debug, debug-assertions, debug-sanitizers.
Rebuilding the JS engine + extensions (after editing GDJS/Runtime or Extensions)
cd GDJS && npm run build # esbuild -> newIDE/app/resources/GDJS/Runtime
# or, while the editor runs, the watcher does this automatically
Packaging
cd newIDE/app && npm run build # react-app-rewired build + check-build-output.js
cd ../electron-app && npm run build # electron-builder
GD_PORTABLE_BUILD=true npm run build -- --publish never # unsigned portable zip
Portable/CLI builds are exposed through newIDE/README.md: --run-command EXPORT_HTML5_EXTERNAL <game.json>
and IMPORT_EXTENSION_AND_SAVE <game.json> --cmd-args <ext.json>.
Code Conventions & Common Patterns
Editor (newIDE/app/src) — Flow, not TypeScript
- Every source file starts with
// @flow. Flow is the type system (1853 annotated files; 5 TS files). .prettierrc:singleQuote: true,trailingComma: es5. Prettier 1.15.3 here (3.x elsewhere).- ESLint is inline in
newIDE/app/package.json(no.eslintrc), extendsreact-app, runs with--max-warnings=0. Notable rules: importTransfrom@lingui/macro(never@lingui/react); never importprop-types; never useconstructor.name(minified in production). - i18n: wrap user-visible strings with
<Trans>/tfrom@lingui/macro.src/locales/**is generated. - State: React Context, no Redux. Feature folders export a context created with
React.createContext(...)and default-export it (MainFrame/Preferences/PreferencesContext.js,Profile/AuthenticatedUserContext.js,ProjectsStorage/ProjectStorageProviders.js); consumers callReact.useContext(XxxContext)directly. The pairedXxxProvider.jsholds state and is mounted inMainFrame/index.jsor the feature root. Reusable hooks live inUse*.js/use*.jsfiles and take an options object (e.g.AiGeneration/UseGenerateEvents.js). - Errors:
showErrorBox({ message, rawError, errorId })fromUI/Messages/MessageBox.js(also reports to analytics).showWarningBox/showMessageBoxfor the softer cases. Plainconsole.erroris fine in non-UI utilities. - Feature flags/entitlements all funnel through
Utils/GDevelopServices/Usage.js(hasValidSubscriptionPlan,limits.capabilities) — check there first when a feature is unexpectedly gated. - Hooks are
Use*.jsinUtils/or the feature folder; components arePascalCase.jsco-located withPascalCase.module.css.
Game engine + extensions (GDJS/Runtime, `Extensions) — TypeScript, garbage-sensitive
- No ES modules. Every file is
namespace gdjs { export class X ... }. Everything shares one global namespace. - Use the
float/integeraliases fromtypes/global-types.d.tsinstead of barenumber. - Written for zero allocation in hot paths — see
newIDE/docs/Supported-JavaScript-features-and-coding-style.md. Avoid object/array spread, object literals per frame, closures created at runtime, shorthand property names. Declare all properties in the constructor (hidden classes). Only allocate once at setup. - Extend and register, e.g.:
Objects likewise:
namespace gdjs { export class DraggableRuntimeBehavior extends gdjs.RuntimeBehavior { /* ... */ } gdjs.registerBehavior('DraggableBehavior::Draggable', gdjs.DraggableRuntimeBehavior); }gdjs.registerObject('TextObject::Text', gdjs.TextRuntimeObject), plus a separatexxxruntimeobject-pixi-renderer.ts. In object constructors callsuper(...)first andthis.onCreated()last. - Extension declaration lives in
JsExtension.jsand must be named exactly that — GDevelop won't load it otherwise. Its contract isExtensionModuleinExtensions/JsExtensionTypes.d.ts. Reference:Extensions/ExampleJsExtension/. - The registration namespace is the first argument of
setExtensionInformation, which often differs from the folder name:Extensions/3D→'Scene3D',Extensions/Physics2Behavior→'Physics2',Extensions/Spine→'SpineObject'. Always verify with theJsExtension.jsbefore writing a type string. - Include-file load order is alphabetical, not declaration order — that is what the
A_/B_/C_/D_filename prefixes are for (Extensions/Firebase/README.md). - 17 extensions still carry C++ declarations (
Extension.cpp+JsExtension.cpp); the list is inExtensions/CMakeLists.txtand mirrored by the link order inGDevelop.js/CMakeLists.txt. Adding a C++ extension also means registering it inGDJS/GDJS/Extensions/JsPlatform.cpp.
C++ (Core/GDCore, GDJS/GDJS, Extensions/*.cpp)
- C++11. File name equals class name (
PascalCase.h/.cpp); namespacegd(Core) orgdjs(GDJS). - Methods
PascalCase, memberslowerCamelCase, private members_lowerCamelCase. - Containers are named
…Container/…ContainersList(VariablesContainer,ObjectsContainersList). - clang-format:
{BasedOnStyle: Google, BinPackParameters: false, BinPackArguments: false}. Note the file is.clang_format(underscore) so clang-format cannot auto-discover it — the style is only applied via the CMake target (make clang-format) and mirrored in.vscode/settings.json. Keep all three in sync. - clang-tidy config in
.clang-tidy; run viacd GDevelop.js && npm run lint(needs a prior build forBinaries/embuild/compile_commands.json).
Cross-cutting: adding C++ API surface
To expose a new C++ class to the editor you usually need only an entry in
GDevelop.js/Bindings/Bindings.idl. If the class needs a wrapper, add the header to
GDevelop.js/Bindings/Wrapper.cpp (generated — add via the build, not by hand-editing glue). Then rebuild
libGD.js. 90% of binding work is the .idl.
Important Files
| File | Why it matters |
|---|---|
Core/GDCore/Project/Project.h |
Root of the C++ data model |
Core/GDCore/Project/Layout.h |
A scene: objects, instances, events, layers, variables |
Core/GDCore/Extensions/PlatformExtension.h |
Every extension declaration goes through this |
Core/GDCore/IDE/WholeProjectRefactorer.h |
Use for any rename/delete that must fix up events project-wide |
GDevelop.js/Bindings/Bindings.idl |
The C++↔JS contract (4648 lines) |
GDJS/Runtime/gd.ts |
The gdjs namespace root, type registries, scene callbacks |
GDJS/Runtime/runtimeobject.ts, runtimebehavior.ts, runtimescene.ts |
Base classes for all extension code |
GDJS/GDJS/Events/CodeGeneration/EventsCodeGenerator.h |
Events → JS transpiler |
GDJS/GDJS/IDE/ExporterHelper.cpp (AddLibsInclude) |
Authoritative ordered list of runtime scripts in an export |
Extensions/JsExtensionTypes.d.ts |
The ExtensionModule contract |
Extensions/ExampleJsExtension/ |
Canonical extension: effect, behavior, object, renderer, tools |
newIDE/app/src/index.js |
Editor bootstrap (Flow, React 18 createRoot) |
newIDE/app/src/MainFrame/ |
Editor shell: tabs, preferences, dialogs |
newIDE/app/src/MainFrame/Preferences/PreferencesProvider.js |
All persisted editor settings + updater IPC |
newIDE/app/src/ProjectsStorage/index.js |
The StorageProvider type — add storage backends here |
newIDE/app/src/Utils/GDevelopServices/ |
All GDevelop backend API clients + ApiConfigs.js (env vars, base URLs) |
newIDE/app/scripts/import-GDJS-Runtime.js |
The script you run after editing the engine/extensions |
newIDE/electron-app/app/main.js |
Electron main: windows, updater, CLI commands |
newIDE/electron-app/electron-builder-config.js |
Packaging + the fork's publish target |
newIDE/electron-app/app/package.json |
Single source of truth for the product version (bump version here only) |
Core/GDevelop-Architecture-Overview.md |
Upstream's own architecture guide — cite it instead of guessing |
Runtime/Tooling Preferences
- npm exclusively. No yarn/pnpm, no workspaces, no hoisting.
BUILD.mdforbids re-introducingnewIDE/web-app/yarn.lock— delete it again if an upstream sync brings it back. CI usesnpm ci. - Node 20 in every GitHub workflow (
.github/workflows/*.yml). CircleCI usescimg/node:24.13.0; Travis/AppVeyor (legacy) use Node 16. No.nvmrc,.node-version,engines, orpackageManagerfield. - Emscripten 3.1.21 (from
juj/emsdk), only needed to buildlibGD.js. .npmrcsetsaudit-level=high,legacy-peer-deps=true(load-bearing for react-scripts 5),save-exact=false.newIDE/appbuilds either viareact-app-rewired(default,npm start/npm run build) or Vite (npm run start:vite/build:vite).- Pre-commit hook (
.husky/pre-commit) runslint-staged→prettier --write+eslint --max-warnings=0on stagednewIDE/app/src/!(locales)/**/*.jsonly. Nothing is checked on commit forGDJS,Extensions,electron-app, or C++ — run those manually. - Environment variables: only
GD_FIREBASE_API_KEYandGD_TEST_FIREBASE_API_KEY(see.env.example,SECRETS.md). No secret values live in the repo; read the key innewIDE/app/src/Utils/GDevelopServices/ApiConfigs.js. - CRA loads
newIDE/app/.env*, not the repo root — a root.env.localis ignored by the app build.
Lint / format / type-check
cd newIDE/app && npm run lint && npm run check-format && npm run check-types && npm run flow
cd GDJS && npm run check-types && npm run check-format # covers Extensions/**/*.ts too
cd GDevelop.js && npm run lint # clang-tidy over Core/GDCore
# C++ formatting is a CMake target, not an npm script:
mkdir -p .build-tests && cd .build-tests && cmake .. && make clang-format
Known red baseline (measured on clean
master).cd GDJS && npm run check-formatlists 41 files andnpm run check-typesreports 322 errors, 305 of themTHREE(Cannot find namespace 'THREE'/Property 'X' does not exist on type '{}') acrossExtensions/3D,GDJS/Runtime/pixi-renderers, etc.; the remaining 17 are pre-existing Spine / pixi typings mismatches. Neither check is gated by the fork's.github/workflows/ci.yml— that job runscheck-formatandcheck-typesinsidenewIDE/apponly;.semaphore/semaphore.ymldoes gateGDJS check-types. Diff against this baseline before blaming your change.
Testing & QA
Six independent suites. Nothing runs ctest; there is no unified test runner.
| Suite | Working dir | Command | Needs |
|---|---|---|---|
| Core C++ (Catch v1) | <repo>/.build-tests |
cmake -DBUILD_GDJS=FALSE -DBUILD_TESTS=TRUE .. && make -j4 GDCore_tests && Core/GDCore_tests |
native toolchain |
| GDJS engine + extension runtime (Karma/Mocha, browser) | GDJS/tests |
npm test (benchmarks: npm run test-benchmark; CI/no-display: xvfb-run --auto-servernum npm run test:chromeHeadlessNoSandbox) |
headless Chrome and a built runtime (cd GDJS && npm run build) |
| TileMapHelper (Karma + karma-typescript) | SharedLibs/TileMapHelper |
npm test |
headless Chrome |
| Bindings + codegen (Jest 29) | GDevelop.js |
npm test |
Binaries/embuild/GDevelop.js/libGD.js (Emscripten build) |
| Editor (Jest 27) | newIDE/app |
npm test -- --watchAll=false --ci |
npm run postinstall → libGD.js-for-tests-only |
| Visual monkey tests (Puppeteer) | newIDE/visual-tests |
npm test / npm run test-storybook / npm run test-editor |
Storybook build or packaged app zip |
Single-test invocations:
# Catch v1 — positional name/wildcard/tag filter
./Core/GDCore_tests "AbstractFileSystem"
./Core/GDCore_tests "[common][events]" # -l to list, -t for tags, -r junit -o out.xml
# GDevelop.js / newIDE/app Jest — positional path regex, -t for test name
cd GDevelop.js && npm test -- __tests__/Vector.js
cd newIDE/app && npm test -- src/AI/CustomAIClient.spec.js --watchAll=false
cd newIDE/app && npm test -- --testPathPattern=CustomAIClient -t "adds a behavior"
# Karma (GDJS/TileMapHelper) has no single-test flag — set client.mocha.grep in karma.conf.js,
# or run `npx karma start` then `npx karma run -- --grep="<pattern>"`
# Visual tests
cd newIDE/visual-tests && node run.js --suite=storybook --test=sprite-editor/delete-animations
Conventions:
- C++: plain
*.cppunderCore/tests/named after the class under test. Shared fixture:SetupProjectWithDummyPlatform(project, platform)inCore/tests/DummyPlatform.h. - Editor:
*.spec.jsco-located with the source (~170 files). Node env by default — DOM specs opt in with/** @jest-environment jsdom */. Only 3 snapshots exist; refresh with-u. - Bindings:
GDevelop.js/__tests__/*.js.jest-light-runnercannot mock modules — use dependency injection andjest.fn/jest.spyOnonly. - Engine/extensions:
GDJS/tests/tests/**/*.jsandExtensions/<Name>/tests/*.spec.js. Mocha +expect.js- global
sinon.
- global
- Reuse, don't reinvent mocks:
GDJS/tests/tests-utils/init.pixiruntimegame.js→gdjs.getPixiRuntimeGame(settings)(memoised, no assets)GDJS/tests/tests-utils/init.pixiruntimegamewithassets.js→gdjs.getPixiRuntimeGameWithAssets(...)GDJS/tests/tests-utils/MockedCustomObject.js,GDJS/tests/tests/Extensions/{TestRuntimeScene,MockedResourceLoader}.jsExtensions/PlatformBehavior/tests/PlatformerTestHelper.js→makePlatformerTestRuntimeScene(stepTime)GDevelop.js/TestUtils/→makeMinimalGDJSMock,generateCompiledEventsFromSerializedEvents,makeTestExtensionsnewIDE/app/src/fixtures/→makeTestProject,makeTestExtensions,GDevelopServicesTestData/newIDE/app/src/EditorFunctions/TestHelpers.js→makeFakeLaunchFunctionOptionsWithProject(project)
- Free wasm objects:
new gd.ProjectHelper.createNewGDJSProject()inbeforeEach,project.delete()inafterEach.
Coverage: only newIDE/app has tooling — npm run analyze-test-coverage → coverage/,
npm run analyze-flow-coverage → flow-coverage/. Both gitignored. No coverage gates in CI.
CI: .github/workflows/ci.yml (fork-owned) gates PRs on lockfile-guard, lockfile-sync,
build-libgd, test-newide-app, test-gdevelopjs, lint-newide-app. .semaphore/semaphore.yml adds Flow
and the GDJS browser tests. .circleci/config.yml carries the heavy upstream pipeline (signed native builds,
clang-tidy, ASan/UBSan, visual tests).
Fork Divergence
This fork differs from upstream in 164 paths out of ~6,700. Expect merge conflicts there.
fork-divergence.json is the authoritative allowlist of every path the fork intentionally diverges
on, in three buckets (modified 110, forkOnly 54, upstreamMissing 7). It is generated and enforced by
scripts/check-fork-divergence.js: the fork-divergence job in .github/workflows/ci.yml fails when a
path diverges that is not listed, or when a listed path stops diverging. Run
node scripts/check-fork-divergence.js after any change that touches upstream-owned files, and
--update (committing the manifest in the same commit) when the divergence is intended. See
MAINTENANCE.md for the full procedure and for why upstreamMissing is the bucket that
silently breaks syncs.
BYOK / local AI — newIDE/app/src/AI/CustomAIClient.js (fork-only directory; ~2145 lines: config in
localStorage under gd-custom-ai-config, the GDEVELOP_OPENAI_TOOLS schema, transformGDevelopMessagesToOpenAi,
parseAssistantMessage, sendChatCompletion, and custom* local implementations of the Generation API).
Plus branch points in AiGeneration/{AiConfiguration,AskAiEditorContainer,AskAiStandAloneForm,Use*}.js,
MainFrame/Preferences/{PreferencesContext,PreferencesProvider,PreferencesDialog}.js (the 'ai' preferences
tab is a fork addition), Utils/GDevelopServices/{Generation,Authentication,Usage}.js. Only spec:
newIDE/app/src/AI/CustomAIClient.spec.js.
Unlocked client features — the master switch is hasValidSubscriptionPlan() returning true in
newIDE/app/src/Utils/GDevelopServices/Usage.js, with UNLOCKED_HERETEK_SUBSCRIPTION /
UNLOCKED_HERETEK_CAPABILITIES seeded in Profile/AuthenticatedUserContext.js. Downstream sites:
Profile/Subscription/SubscriptionChecker.js, ProjectManager/LoadingScreenEditor.js (watermark + splash
clamp removed), ExportAndShare/LocalExporters/LocalPreviewLauncher/index.js (hot-reload paywall + network
preview), MainFrame/EditorContainers/DebuggerEditorContainer.js.
Auto-updater feed — mechanism is upstream-shared (electron-updater in newIDE/electron-app/app/main.js,
MainFrame/UpdaterTools.js). Only the publish target differs:
newIDE/electron-app/electron-builder-config.js → owner: 'Heretek-AI', plus Heretek release links in
MainFrame/{AboutDialog,Changelog/*}.js.
Upstream sync (.github/workflows/upstream-sync.yml, daily, PR-only — master is never auto-modified):
- merges
upstream/masterintoupstream-sync-<date>; - restores
.github/workflows/from local master — never edit workflows expecting upstream changes to land; - verifies five BYOK files still exist and restores them if a merge dropped them —
src/AI/CustomAIClient.js,src/AI/CustomAIClient.spec.js,src/AiGeneration/AiConfiguration.js,src/MainFrame/Preferences/PreferencesDialog.js,src/MainFrame/Preferences/PreferencesProvider.js. Renaming or moving those five paths silently disables the guard. - verifies fork divergence against
fork-divergence.json(informational, via the stepVerify fork divergence is unchanged by the sync).
Also note: extract-translations.yml, update-translations.yml, update-extension-translations.yml and
gdcore-tools-hook.yml are guarded by if: github.repository == '4ian/GDevelop' and never run here —
extract/compile translations manually. Lockfile policy (BUILD.md): npm only in newIDE/web-app,
npm ci in CI, keep the overrides block in newIDE/app/package.json, validate with
node scripts/security/check-lockfiles.js.
Documentation Sources
Cite these rather than inventing behaviour:
Core/GDevelop-Architecture-Overview.md— the architecture primer (IDE vs Runtime,gd::Variablevsgdjs.Variable, events, GDevelop.js).newIDE/README.md— install/run, Electron, Storybook, portable + CLI builds, deploy, translations.newIDE/README-extensions.md— extension authoring, file naming, the dev loop.newIDE/docs/Supported-JavaScript-features-and-coding-style.md— the garbage-free engine rules.GDJS/README.md,GDJS/tests/README.md,GDevelop.js/README.md— engine, tests, bindings.BUILD.md,SECRETS.md,KNOWN_VULNS.md— fork build/security policy.MAINTENANCE.md— how to keep this fork maintainable: fork-divergence policy and the guard that enforces it, the fallow and SonarCloud scopes, what to fix vs. leave in each scanner, and the upstream sync procedure. Read it before editing.fallow.toml,.sonarcloud.properties,fork-divergence.json, or.github/workflows/.Extensions/README.md,newIDE/README-themes.md,newIDE/visual-tests/README.md.- Root
scripts/README.mdis partially stale — it referencesReleaseProcedure.{bat,sh}andCopyWindowsToLinuxReleaseFiles.sh, which do not exist in this checkout.