vike
Scripting

User strategies

A user's strategies are files under user_data/strategies, not rows in a blob — the layout, the two trees, the scan that compiles every script, and its named diagnostics.

crates/vike-studio-core/src/user_strategies/mod.rs opens with the rule the whole module exists for: a user's strategies are files, not rows in a blob. It is the loader for <project>/user_data/strategies/, the resolver that turns one of its presets into constructed params, and the one-time migration off the Studio's studio_strategies.json. The directory it scans is the user-content root vike_model::state_path::PROJECT_USER_DATA_DIR defines.

Why files

The header records what the JSON array denied a person: a script in it could not be opened in an editor, diffed, committed, copied to another machine or sent to somebody. Two properties made it lossy rather than inconvenient. One corrupt byte lost the library, because the blob loader answered a parse failure with an empty list — right for a window layout, wrong for the only copy of somebody's work — while one file per strategy costs exactly the strategy the bad byte is in. And it was filed under the wrong owner, at the hist store's root, beside re-downloadable market data whose lifecycle is "delete it and re-ingest". Migration is the move.

The layout

user_data/strategies/rhai/<name>/<name>.rhai      the ENTRY file — its stem IS the strategy name
user_data/strategies/rhai/<name>/*.toml           presets, flat beside the entry
user_data/strategies/rhai/<name>/presets/*.toml   …or filed, for the user whose sweep left forty
user_data/strategies/rust/<builtin>/*.toml        presets for a strategy whose code is in the BINARY

Four rules the loader depends on. The entry file matches its folder, so a folder holding several scripts is never ambiguous — the others are helpers, read by nothing here. A .toml beside the entry is a preset for that strategy, so rm -r <name>/ removes the whole thing and two strategies' presets can never be confused. A preset is the params table, flat. And a folder under rust/ needs no entry file when its name is a built-in strategy: the code is in the binary, resolved by name through the harness registry's strategy_by_name, and the folder holds nothing but presets. load.rs's header admits that treating such a folder as a missing entry file was a real bug the module used to have, and that migrate.rs predicted it.

state_path.rs's STRATEGIES_SUBDIR doc gives the reason for the language split: the two have different lifecycles — a .rhai file is read at startup, a .rs file must be compiled — and the tree is artifact first, language second. user_rust_strategies_dir is explicit that the rust/ tree is meaningful only in a source checkout; on an installed binary it is inert, and that is not a defect. See Authoring in Rust.

vike-cli init scaffolds the tree with three examples — sma_cross, rsi_meanrev and ema_trend, each with presets — plus rust/my_experiment/. By default it writes only what is absent, so an edited file is never touched; --reset is the one exception, restoring the shipped samples and overwriting edits to them (a strategy you wrote is not in that table, so nothing here can delete it) — vike-cli init. VIKE_USER_DATA_DIR names the root outright and skips the walk, a separate variable from VIKE_SETTINGS_DIR on purpose.

The scan

load_user_strategies(strategies_root) scans both trees, rhai/ then rust/, into one LoadReport, because a strategy is addressed by name and the two trees share one namespace. load_rhai_strategies(root) is the Rhai tree alone and cannot see the rust/ presets. Neither ever fails; every failure is a row. load.rs's header states three decisions that look like details and are not:

  • An absent root is not an error; an unreadable one is. A fresh install has no user_data/, so "nothing there" stays silent; a root that exists and cannot be listed is a permissions bug wearing the "not configured yet" answer, and it gets its own RootUnreadable row. That is the distinction the credential store draws, for the same reason.
  • Every script is compiled during the scan, through run.rs's build_strategy — the same call a Run makes — so "it loaded" and "it will run" are one claim rather than a failure moved to the moment the user presses Run.
  • A rust/ folder named after a built-in needs no entry file. StrategyBody is the type that says which kind a loaded folder is: Rhai { entry, source }, proven to compile, or Native, whose UserStrategy::name is the registry's own spelling however the folder was cased.

Folders are sorted by name before anything loads, dot-entries are skipped as tool droppings, and names are claimed case-folded across both trees. A folder that loaded nothing claims no name, so a later folder of the same name is a real strategy rather than a duplicate of a failure.

The diagnostics

LoadDiagnostic has one variant per mistake a person actually makes, each carrying the path and the fix, and Severity draws the line at "did the user lose a strategy": BadPreset and ShadowedPreset are warnings that leave the strategy runnable, everything else is an error.

variantwhat happened
RootUnreadablethe root exists but cannot be listed — fix its permissions
MissingEntrya rhai/ folder holds no <name>.rhai; the message lists the .rhai files it did find, so a misnaming explains itself
NativePresetsMisfiledpresets for a built-in filed under rhai/; the fix is to move the folder to strategies/rust/, which is why this is its own row rather than a MissingEntry wording
UnregisteredNativea rust/ folder holding presets whose name is no built-in — they can reach nothing
UnreadableEntrythe entry file is there but is not readable as UTF-8 text
CompileFailedRhaiStrategy::compile rejected it, via build_strategy; the script author's message verbatim
BadPreseta .toml could not be read, parsed or used as a params table; the strategy loads without it
ShadowedPresetthe same preset name flat and under presets/; the flat one wins as the documented primary position
DuplicateNametwo folders resolve to one name — SmaCross/ and smacross/, which coexist only on Linux, or rhai/grid/ beside rust/grid/; the rhai/ claim wins
StrayScripta script directly in a tree's root; the message names the folder it belongs in

A rust/ folder with no presets and no registry name is not reported

load_rust_folder is the one place the loader returns nothing without a diagnostic, deliberately: a folder naming no built-in and holding no presets is a source-checkout Rust strategy that has not been registered yet — the shipped my_experiment/ template's exact state — and nothing reads that tree at runtime. The moment it holds presets, they are addressed by a name that resolves to nothing, and that is a loss the UnregisteredNative row reports.

The compile log

render_compile_log(report, stamp) renders one scan as the text a caller appends to user_data/logs/compile.log: one ok line per loaded strategy naming its entry file (or built-in) and its presets, one ERROR/WARN line per diagnostic, and a summary count. Passes are logged too, because a user debugging "why is my strategy not in the list" needs to see the ones that are — an absent name is the answer. stamp is caller-supplied and the function reads no clock, one stamp per line so grep <name> still answers "when". The module performs no logging and opens no log file; the composition root decides where the text goes. user_logs_dir's doc adds that $VIKE_LOG_DIR must not redirect this folder, because that variable moves the app log.

A loaded strategy's spec() is the bridge to a Run — the Rhai source, or the registry name with an empty params table — and applying one of its presets is Presets. The Studio's own restorable state, and the blob this tree replaces, are on Saved strategies and workspace.

On this page