vike
Scripting

Migration

The one-time move of the Studio's studio_strategies.json rows into user_data/strategies/ — what each row becomes, what is reported instead of written, and why the JSON is never touched.

Before a user's strategies were files, every one the Studio saved lived in a single pretty-JSON array colocated with the data store as studio_strategies.json (crates/vike-studio/src/saved.rs's save_strategies). crates/vike-studio-core/src/user_strategies/mod.rs names the two properties that made that shape lossy: load_strategies answers a parse failure with an EMPTY list, so one corrupt byte loses the whole library; and the blob sits at store.root(), inside the hist store — re-downloadable market data whose lifecycle is "delete it and re-ingest". The migration in crates/vike-studio-core/src/user_strategies/migrate.rs moves those rows into the layout User strategies describes, once.

Two populations, two destinations

The blob holds one row per "saved strategy", but the rows split into two kinds that only look alike (migrate.rs's module header):

rowwhat the user actually authoredbecomes
Rhai — carries the scripta strategystrategies/rhai/<name>/<name>.rhai, the script byte-verbatim
Native — carries a registry name plus free-form param rowsa parameter setstrategies/rust/<native>/<entry-name>.toml, a preset for that built-in

A native row carries no code and cannot: the strategy is compiled into the binary. Migrating it as a strategy folder would produce an empty folder named after something the user never wrote, so it becomes a preset filed under the registry strategy it belongs to. That folder holds presets and NO entry file, and the loader treats it as a legitimate second shape (load.rs's StrategyBody::Native) — migrate.rs predicted that rule in its own doc before a rust-side loader existed, and load.rs's header records that copying the rhai rule verbatim was a real bug.

The script is copied verbatim, because a migration that reformats or annotates a user's script has rewritten their work.

Text rows into a TOML preset

The JSON's params are (String, String) pairs — text, because there is no typed ParamSpec seam to check them against. The migration invents none: preset_toml runs the rows through crates/vike-studio-core/src/spec.rs's params_from_rows, the exact function the Studio uses at run time, and serialises the result through a toml::Value. So the migrated file MEANS what the JSON row meant: 2 stays an integer, true stays a bool, and a bare BTCUSDT — not valid TOML on its own — becomes the quoted string the run-time path would have made of it. A value with a quote in it, or a leading #, is escaped by the serialiser rather than pasted into a template.

The table is FLAT (size = 2, never a [params] wrapper), the one preset format both trees share (Presets). Above it sit two provenance lines, because six months later "where did this come from" has to be answerable from the file itself:

# preset 'hold-2' for the built-in strategy 'buy_hold'
# migrated from studio_strategies.json
size = 2
symbol = "BTCUSDT"

Both names are folded onto one line first (one_line replaces every control character with a space): a name carrying a newline would end the comment and turn a provenance note into a TOML parse error.

Names become filenames

The Saved pane never constrained its name box, so a row can be called BTC / ETH pairs (v2) — or ../../secrets. slug has one rule: anything that is not a letter, a digit, -, _ or . collapses to a single -, then leading and trailing -/. are stripped. That removes every path separator, reserved character and control character without a deny-list, and the strip makes traversal structurally impossible — .. cannot survive at either end. Letters are Unicode, not ASCII; a strategy named in Cyrillic is a perfectly good filename. From the module's own pins: BTC / ETH pairs (v2) becomes BTC-ETH-pairs-v2, ../../secrets becomes secrets, and ... or /// leaves nothing — reported as Unnameable, never silently renamed to something the user would not find again.

What is reported instead of written

plan_migration is PURE — no filesystem, no clock, no environment — and produces paths relative to <project>/user_data in the input's order, so a report reads in the order the Saved pane showed. apply_migration joins the root, which is also what confines every write under it. Every failure on either side is a named MigrationSkip, never an Err: one unwritable file must not abandon the other nineteen.

skipwhenthe message says
Unnameablethe name has no usable filename formrename it in the Saved pane and migrate again
NamelessNativea native row whose registry name is blanknothing for the preset to belong to
Collisiontwo rows plan the same filewhich row already claims the path; rename one of them
AlreadyPresentsomething is already at the target pathalready migrated, left untouched
WriteFailedthe directory or the file could not be writtenthe path and the error
PresetNotSerializablea native row's params are not expressible as TOMLthe serialiser's error; re-create the row by hand

Collisions are case-FOLDED: Fast and fast are two files on Linux and one on Windows, and a migration that silently overwrites on one platform and not the other is worse than one that refuses. Saved names were never unique-constrained and slug maps distinct names onto one stem (my strat and my/strat both become my-strat), so this is reachable from ordinary use. The first row wins; every later one is named.

Idempotent, and the JSON is read-only

A migration that consumes its source cannot be re-run

apply_migration writes a file only where NOTHING is at that path — probed with symlink_metadata rather than exists(), so a dangling symlink is not written through. A second run writes nothing, reports every target as AlreadyPresent, and leaves a user's later edits to a migrated file intact; MigrationOutcome::written is empty on a re-run, which makes the contract observable rather than asserted. The module never reads, writes, moves or deletes studio_strategies.json — it does not even take its path. The blob stays the read-only fallback for a build the user rolls back to.

Where the rows come from, and what loads back

The migration takes already-parsed LegacyEntry rows rather than the Studio's own SavedStrategy: that type lives one layer up, in the crate that owns the JSON schema and its back-compat contract. crates/vike-studio/src/saved.rs's SavedStrategy::legacy_entry is the one conversion, kept beside the schema so the rule that a row with no source key is Rhai has exactly one authority. This page names no menu item or command, because its authority names none; the Studio side is on Saved strategies and workspace.

migrate.rs pins both round trips end to end: a migrated Rhai row is found by load_rhai_strategies, compiles with no diagnostic and reports its source verbatim; a migrated native row loads as a native folder, and resolve_preset for buy_hold / hold-2 hands back size as the integer 2 and symbol as the string BTCUSDT, and builds the strategy it presets.

On this page