Contributing
For specs in your own application, see Testing applications. This guide covers development and verification of CryKit itself. For changes to the documentation website, see Maintaining the docs.
For framework design, see Architecture and How it works. Rendering describes retained templates and updates; Runtime protocol specifies the Crystal/browser message contract.
Repository map
| Location | Responsibility |
|---|---|
src/crykit/ | Public application API and subsystem entry points |
src/crykit/view/ | Public view extension macros |
src/crykit/views/ | Mounted view lifetimes and registry ownership |
src/crykit/execution/ | Lifetime scopes, delivery queues, worker scheduling and cancellation |
src/crykit/content/ | Resource navigation and buffered responses |
src/crykit/transport/ | Native document bridge, adapter contract, resource translation and browser runtime |
src/crykit/assets/ | Asset embedding and change watching |
src/crykit/native/ | Native build hooks, file notifications and bundled WebView loading |
src/crykit/capabilities/ | Application capabilities and their backends: windows, dialogs, system, menus and trays |
src/crykit/events/ | Event protocol, dispatch, channel delivery and scoped listeners |
src/crykit/templates/ | ECR compiler, retained bindings and regions, template sessions and literal updates |
src/crykit/reactive/ | Reactive dependency sources and consumer publication |
pkg/crykit-webview/ | Owned native WebView engine, Crystal bindings, and engine tests |
ext/ | Native shims |
make/ | Focused build, runtime, native, desktop and benchmark targets |
runtime/src/ | TypeScript event connection and DOM interpreter |
runtime/dist/ | Committed, self-contained runtime bundle |
runtime/tests/ | Browser contracts and Crystal fixtures |
tests/ | Native regression fixtures and desktop smoke applications |
examples/demo/ | Complete application example |
examples/tray/ | Windowless macOS status items with native menus and click listeners |
benchmarks/ | Crystal benchmark fixtures and recorded comparison |
doc/ | Framework guides and implementation documentation |
Keep implementations in their owning namespace and directory. Public types such as Application, View, ReactiveState, Job, AppJob and JobExecutor retain their documented names. The capabilities/ directory is an organizational group for the existing Windows, Dialogs, System, Menus and Trays namespaces. Internal types and direct implementation-file imports are unsupported application APIs; use require "crykit" in application code.
CLI validation and other expected command failures use CryKit::CLI::Error. Command entry points call CryKit::CLI.handle_errors to print Error: <message> to standard error and return status 1. This boundary also handles option parsing, YAML parsing, and I/O errors. Helpers still raise their errors so callers can handle them; unexpected exceptions propagate for diagnosis. Keep new expected failures typed explicitly rather than rescuing every exception.
Keep resource loading independent of mounted rendering: Content depends on registry owner/document identities, while handlers load the full registry. DOM guards use the lightweight execution context without loading worker jobs. Contracts and injected managers must continue compiling without native engines.
Development setup
Complete the Crystal and WebView setup. The engine lives in pkg/crykit-webview, ships inside the CryKit shard, and is developed alongside CryKit. Applications declare only a crykit dependency; they do not install the engine as a separate shard. Install Ameba for make lint. Runtime development additionally requires Node.js 22.12.0 or newer; the runtime package declares npm 11.18.0. Native build-cache tests require Ruby with Minitest. Install the contributor dependencies explicitly:
make setupThis runs shards install, installs locked npm dependencies, and installs Playwright's Chromium and WebKit. PLAYWRIGHT_INSTALL_FLAGS can supply browser installer flags. Checks do not install dependencies or regenerate the committed runtime bundle.
Native shim builds
The shard's postinstall hook builds the bundled WebView engine and native shims. Crystal compilation also invokes their native builds, so path dependencies and changed sources work without a manual build command. make webview-build and make native-build remain available for focused development.
On macOS, Crystal builds automatically invoke make for the native shims under ext/darwin. Unchanged object files are reused. Changing a .m source rebuilds its object. Window feature implementations live under ext/darwin/windows/, each owning its native state and cleanup; the small windows.m entry point preserves teardown order. The build combines their objects into the existing windows.o link boundary. Changing a window feature recompiles that feature and relinks the aggregate; changing windows/internal.h recompiles all window features. Changing the native Makefile, compiler selection, or compiler flags rebuilds all shims. SDKROOT and MACOSX_DEPLOYMENT_TARGET changes also invalidate the objects. A failed compile preserves the previous object and reports the build failure.
Use make native-clean after replacing a compiler or SDK in place at the same path. The build records the selected command and configuration; it does not inspect toolchain contents for changes.
make native-build-test runs the real native Makefile in temporary directories with a fake compiler. It requires Ruby with Minitest and make, and checks cache reuse, invalidation, failure recovery, and overlapping builds of the same configuration without the macOS SDK or a GUI. It is included in make check.
Desktop smoke fixtures use tests/support/native_probe.cr and tests/native_probes.mk to build only their native probe. Cached probe objects live in the ignored tests/.native/ directory, separate from production shims. These builds honor CLANG, CPPFLAGS, OBJCFLAGS, SDKROOT, and MACOSX_DEPLOYMENT_TARGET, with the same configuration tracking and atomic publication as the shims. make native-build-test checks both build caches. Use make native-probes-build for a compile-only check of all probes, and make native-probes-clean after replacing a toolchain at the same path.
The WebView engine has the same configuration tracking and atomic publication for its native objects and archives. Its separate make -C pkg/crykit-webview native-build-test covers compiler/SDK changes, failed compilation and linking, and overlapping builds; make webview-check includes it. Use the package's make clean after replacing a toolchain at the same path, and separate checkouts for simultaneous builds with different configurations.
Runtime build and embedding
Edit TypeScript under runtime/src/, then run:
make runtime-build
make runtime-check
make runtime-consumer-checkCommit the regenerated runtime/dist/crykit-runtime.js with source changes. Vite produces one self-contained IIFE without imports, dynamic imports, or source maps. The syntax target is Safari 14, Chrome 87, and Edge 87; this is not a claim of complete native platform verification. The bundle freshness check rebuilds for comparison without repairing the committed file.
CryKit::Transport::Runtime::SOURCE embeds the shipped file through ECR at Crystal compile time. Application consumers do not need a Node.js build step. The consumer check compiles and runs an embedding fixture outside the repository working directory and compares its bytes with the committed bundle.
Browser setup compiles runtime/tests/fixtures.cr once before starting workers. The generator calls each fixture's RuntimeFixtures method and emits a JSON object of payload strings, which setup validates and exports through the existing CRYKIT_* environment variables. Keep fixture construction inside its method so local variables and state stay scoped to that fixture. To inspect all generated payloads directly, run crystal run --error-on-warnings runtime/tests/fixtures.cr.
Changing the template engine
Application markup is ordinary HTML with the bindings and region directives described in Compiled templates. The browser owns HTML parsing; the compiler scans only the supported ECR forms and records explicit target markers.
Changes to template syntax belong in templates/compiler.cr. Keep diagnostics, byte offsets, quoted/percent Crystal expressions, and raw HTML contexts covered by the compiler specs. The generated Crystal executes in the caller's lexical scope; paths resolve relative to that Crystal source file.
Retained bindings and regions live in templates/instance.cr, with publication coordinated by templates/session.cr. Evaluation stages values, dependency subscriptions, callbacks, and child owners before publication. Keep failed candidates from changing committed output or subscriptions. Preserve keyed DOM identities and view lifecycle scopes across successful updates.
Protocol changes require matching updates to Crystal mutations and the browser interpreter. Test both generated HTML and real Crystal mutation batches in Chromium and WebKit. Registration must reject missing, duplicate, wrongly typed, or misplaced managed targets before publishing an insertion. See Runtime protocol for the wire contract.
Development literal updates also need matching compiler-signature, watcher, and mounted-update tests. Compatible changes cover eligible ordinary text and the presentation attributes class, style, title, placeholder, alt, role, and aria-*. Changes to structure, actions, bindings, or other source require a rebuild. This mechanism updates declared literal bindings; it does not morph arbitrary static DOM.
Verification
Use make for the common workflow and make help-all for the complete grouped command catalog. During development, run the Crystal suite or a focused spec:
make test
make test SPEC=spec/capabilities/windows/window_focus_spec.crUse make format to format Crystal sources. For the full automated verification path, run:
make verifyThis runs two stages in order:
make check— regression suite. Checks formatting, Ameba and capability contract separation; builds and checks the WebView engine, including Crystal specs, JavaScript bindings, example compilation and macOS native tests; runs CryKit specs, native shim build-cache tests, clean shard-consumer integration and native capability regressions; then checks TypeScript, committed bundle freshness, Chromium/WebKit contracts and Crystal embedding.make desktop-smoke— real desktop suite. Runs activation/lifetime scenarios, resident trays, lifecycle and resource cleanup, window controls, appearance, behavior, events, focus, close interception and geometry, plus native content, menus, automated file drops and the real demo's WebKit interactions (make demo-smoke).
On macOS, check includes native AppKit/WebKit checks and requires a logged-in desktop session. On other platforms it runs the available regression checks. verify requires macOS and foreground access; let the fixtures control focus. Make keeps the suites sequential even with -j. Activation fixtures use fresh unbundled processes, so Launch Services and LSUIElement packaging need separate verification. Browser or fake-backend tests alone do not establish native OS support.
For framework changes, run focused specs and make check; run make verify before considering native lifecycle work fully checked. For documentation-only changes, check links, examples and git diff --check. Keep user guides aligned with implemented APIs and discuss future designs in a repository issue.
Focused checks
| Area | Command | Included in |
|---|---|---|
| Engine specs, bindings, examples and native engine tests | make webview-check | check |
| Failed provisional navigation, renderer termination and asynchronous file-input sheets | make -C pkg/crykit-webview lifecycle-test | check on macOS |
| Injected managers, capability contracts and bridge compile without native engines | make capability-contracts-check | check |
| Source parsing, retained literal reload, CLI rebuild and failure recovery | make source-reload-check | check |
| Shim cache reuse, invalidation, failure recovery and overlapping builds | make native-build-test | check |
| Fresh applications install and compile with only a CryKit dependency | make shard-consumer-check | check |
| Native capability boundaries | make native-<capability>-test for dialogs, windows, system, menus or trays | check on macOS |
| TypeScript, bundle freshness and browser contracts | make runtime-check | check |
| Compiled Crystal embedding matches the shipped bundle | make runtime-consumer-check | check |
| Regular/Accessory activation, hidden readiness and resident lifetime | make application-activation-smoke | desktop-smoke |
| Resident trays and native menu interactions | make trays-smoke, make menus-smoke | desktop-smoke |
| Native document readiness, navigation/reload, local assets and CSS hot reload | make native-content-smoke | desktop-smoke |
| Demo navigation, theme inheritance and window playground | make demo-smoke | desktop-smoke |
| Standalone notes editing, debounce, selection, deletion and JSON import/export | make notes-smoke | desktop-smoke |
make shard-consumer-check uses a clean source checkout and temporary application fixtures to verify both Git and path dependencies. Each fixture declares only crykit, installs with Shards, and compiles an application using require "crykit" without prebuilt native artifacts or a manual engine build. It checks the shipped runtime as part of the consuming application and requires Ruby with Minitest, Git, Shards, Crystal, and the native build tools.
Individual window fixtures can also run directly, for example:
CRYKIT_DESKTOP_SMOKE=1 crystal run --error-on-warnings tests/window_focus_smoke.crOther focused fixtures cover controls, appearance, behavior, events, close interception and geometry. They are included in desktop-smoke. Focus checks briefly activate Finder and automatically dismiss a test dialog. The native content fixture checks document readiness, navigation, and reload.
Manual native review
These checks require interaction or visual inspection and run separately from make verify:
| Command | Review |
|---|---|
make dialogs-smoke | Accept and cancel file/directory selections; check starting directories, filters, default save filename, messages, confirmation keys, and custom question actions. |
make file-drop-smoke | Drop files from Finder onto declared targets; check disabled receiving and drops outside targets. |
make window-drag-smoke | Drag from declared regions; check control exclusions and the configured titlebar double-click action. |
crystal run examples/tray/src/main.cr | Check monochrome icons in light/dark appearance, left/right clicks and popup menus. |
For the appearance comparison, run:
CRYKIT_DESKTOP_SMOKE=1 CRYKIT_APPEARANCE_VISUAL=1 crystal run --error-on-warnings tests/window_appearance_smoke.crThis holds the final dark-transparent/light-opaque comparison for 45 seconds.
Benchmarks
Benchmarks are optional measurements and are not included in make verify.
make benchmark-compile runs the compiled-template growth fixture with 8, 16, and 32 view types. Each generated executable mounts its views, dispatches callbacks, and checks the resulting values. Builds use separate temporary compiler caches and record compiler statistics and LLVM output. The required compile-growth-check uses this fixture without a wall-clock threshold. Pass options with COMPILE_BENCH_ARGS, for example make benchmark-compile COMPILE_BENCH_ARGS='--counts 8,16,32 --keep'.
make benchmark-runtime measures sparse target registration and range reversal, removal, and replacement using the shipped runtime in WebKit. Run npm run benchmark -- chromium 1000 5000 from runtime/ to choose another engine or list sizes. Rebuild the runtime after source changes; the benchmark uses the same installed browsers as the tests. HTML and payload construction, transport, layout, and painting are outside its measured intervals.
make benchmark-rendering measures compiled ECR scalar updates and unchanged keyed-list invalidation as unrelated rows grow. See the fixture and correctness boundaries; the scheduling specs also count actual part visits instead of inferring sparse work from unchanged output.
Other focused measurements are make benchmark-notes for the real editor's input and queued-save path, make benchmark-reactive-dependencies for dependency tracking and publication, and make benchmark-native-assets for resource delivery. Inspect each fixture's checks and measured scope before comparing results. Use the same machine and compiler, run comparisons serially, and retain all results.