Unreleased [Not released yet]
Part 4 of 11. Previous part
Fixed (464)
- A plugin
pathreads as each other string of Cargo.toml:'''../k'''and"""../k"""are../k. An empty path is an error that names it. Before, such a path was the app folder, and the error named another cause. - A
[dependencies.kit]table takes the dependency from the workspace only withworkspace = true. Before, any such table took the rename of the workspace'skit, also withworkspace = false. - A release build reads
[lib]and[workspaceonly as table headers of Cargo.toml. Before, the text in a comment or a string made a plain build, or took the folder above as the workspace root. - A cargo child with no output to read is a failed build with the message
Could not read the output of cargo <command>.Before,wisppanicked. wisp::oauth::oidcwith a name that is not a word (my-okta) answers 500 witha provider's name is a word, such as "github", notmy-okta. Before, the handler panicked after the discovery fetch.wisp dev: after a failed build, a save in a layer reaches the app again. The check before the compile now copies the layer in first. Before, it read the broken copy of the failed build, and each later save failed until you copied the files by hand.wisp lspdrops the data of an app when the editor closes the last open file of that app. Before, the server kept the data of each app until it stopped.- After
shutdown,wisp lspanswers each request with the error -32600 (InvalidRequest). The LSP specification asks for this error. Before, the server answered the request. - The colors of
wisp lspkeep the names of a block after a browser text hole such as{:count}. Before, thecharof{#each xs as char}showed as a type after{:count}. wisp lspreads the name of an action attribute as the build does:#[action]and#[wisp::action]mark an action,#[actionx]does not. A comment between#[action]andfnno longer hides the action.- The build reads a
mod NAME;ofmain.rsandlib.rsby tokens, at the top level only. The braces of a macro (cfg_if!) are not a level. The body of a fn or aconstis a level. Amod NAME;in a string literal or in an inline module is not a declaration ofsrc/NAME.rs. Amodwith its name on the next line is one, and so ismod r#NAME;. Before, the build did not compilesrc/NAME.rsfor such a string, and compiled it a second time for such a declaration. - The auto-imports take
lib.rsas the crate root only when its code callsapp!. Before, a comment or a string withapp!inlib.rswas enough. The auto-imports also read amod NAME;in the braces of a macro, but not in amacro_rules!body. - In a new app, CLAUDE.md and the Cursor rule
.cursor/rules/wisp.mdcnow sayRead @AGENTS.md for the whole Wisp reference.Claude Code loads AGENTS.md from this line. Before, a colon came right after@AGENTS.md, and Claude Code loaded nothing from the line.wisp update-docsdoes not change a pointer file that is there. In an older app, change the line by hand. - In a Rust expression of a template, the build reads
<as less-than. A<opens a bracket only in a turbofish (::<) and at the start of a qualified path (<T>::f()). It also opens a bracket in a type afteras,->or a single:. A>that closes no<ends the type, and so does a+,-,*,/,%,^or&after the type name. Pointer and reference marks (*const,*mut,&'a) stay in the type. The parameters of a closure (|a, b|) hold their commas.{@debug a < b, c}prints 2 values, and{@debug f(a > b, c)}prints 1. A{@props}default can hold<. The arguments of{:@render}are JavaScript. In them,<is less-than, and a string in'or a backtick holds its commas, also in a${ }of a template literal. A comma in a raw string or in a comment does not split the Rust arguments of a block. The build reads each variant of an enum whose discriminant holds<(A = 1 << 2, B). Before, the build split these lists at the wrong commas. - The build reads each key of Cargo.toml with the same rules for comments, table headers, quotes and strings over lines. A comment after
[package]no longer makeswisp openapiwrite the nameappand the version0.1.0. This applies when the environment has noCARGO_PKG_NAMEandCARGO_PKG_VERSION. A line of a list or of a string over lines no longer ends[package.metadata.wisp]or hides the tables after it. A header such as["x"]now ends it for each key. Blanks and quotes in a header do not count:[ package ]and["package"]are[package]. The dotted keypackage.namebefore the first table counts too, and a byte order mark before the first line is not part of it. A quoted key such as"kit"iskit. The dependency tables are[dependencies],[dev-dependencies]and[build-dependencies], each also under[target.<cfg>], and[workspace.dependencies].[workspace.dev-dependencies]and[package.metadata.x.dependencies]are not dependency tables now. A plugin path and a library rename can use a dotted key:kit.pathandkit.package. A rename can use single quotes:package = 'x'.workspace = falseno longer takes a dependency from the workspace. A string in"""or'''can end with up to 2 more quotes, as TOML allows.wispreads the package name (for the path remap and the exe pick of a release build). The build reads the dependency tables. Both use the same rules. Before, each key had its own rules. - Edge hosts: the kept answer of a
constpage readsif-none-matchas the server does. OneW/at most comes off a tag,W/*is not*, and a comma inside the quotes of a tag is part of it. Before,W/W/"x"andW/*got the kept 304. - Lambda behind an ALB without multi-value headers: a reply holds at most 512
set-cookieheaders, one under each case of the letters of the name. Past 512, Wisp sends the first 512 and logs one line for the reply. Before, cookie 513 and later used a key again, and the ALB dropped them. - Deno KV table store: a write splits into atomic commits under the limits of Deno KV (800 KiB of keys and values, 80 KiB of keys). Before, a commit held 100 rows of any size, and Deno KV refused a commit of large rows.
- A lint level reads as each other string of Cargo.toml:
a11y = '''off'''isoff. A line inside a string over lines of[package.metadata.wisp]is no key. Before,lint = 1inside such a string was an error. wisp devreads the answer of the app to a swap (/_wisp/dev/swap,/_wisp/dev/module,/_wisp/dev/lits) also when the app closes before it reads the whole body (a 413). Before, the reset of the connection dropped the answer, and the swap got no status.- The
Hostcheck of thewisp devfront andwisp preview --staticuses the rule ofwisp_shared::loopback. A port has 1 to 5 digits, solocalhost:123456gets a 403.a@b.localhostandLOCALHOST.get a 403 too. - Gate: each step finds the Wisp checkout from the current directory. Before, it used the folder of the build. A gate binary that a deleted worktree built failed with "cannot change to ...".
wisp::fetchreads a reply as chunked only whenchunkedis the last coding oftransfer-encoding(RFC 9112 6.3). With another last coding it reads the body until the server closes and ignorescontent-length. The 64 KB limit of the reply head now counts the interim heads (such as 103) too, also for a head that comes whole in one read. Before, only a head that came in parts had a limit, 64 KB for each head.wisp::spawnoutside a tokio runtime logs a line and does not run the task. Before, it panicked.WISP_SPAreadson,1,trueandyes, as the other switches. A value that is not on or off stops the export with a message.- An
fsstore whose escaped name is longer than 254 bytes keeps its files in a folderg<sha256>, as a long key does. Before, each call failed with a 500, because the folder name was past the 255 bytes of a file name. /_imgtakes its resize slot after a remote fetch, so a slow origin holds no slot that local misses need. With every slot taken, a miss gets its 503 before the fetch, so a full server sends no request to the origin. The cache counts the bytes of its keys against its 64 MB. A remote key and its fetch have the host in lower case, so one picture has one entry. A file ofstatic/that grows after the size check stops the read past 10 MB (a 400).- On Linux with glibc, a worker that had 170 or more connections open at one time now lets the clock thread trim the heap. The trim comes after 2 s with no connection open. Before, only a shrink of the buffer pool or of the spare write buffers started a trim. So the freed heap of the peak stayed.
- When the OS cannot start the clock thread, the server stops at start with the message
wisp: cannot start the clock thread: <OS error>and exit code 1. Before, it panicked. - When the OS cannot start a thread, an
fsstore call fails with 503, and an image resize answers 503 withretry-after: 1. The DNS lookup ofwisp::fetchfails ascould not resolve. Before, tokio panicked in the request. A thread that started later could then run the queued store call after the error, and overwrite a newer write. Now a write or delete call does nothing when it starts late. When a thread took the call before that, the answer waits for the end of the call. It is 503 withfs: the call ran, but its result is lost. A read has no write to lose: a read that starts late runs as waste only. A panic in theinitofsrc/hooks.rsstops the server at start. It writeswisp: init in src/hooks.rs failed: panic at <place>: <message>and exits with code 1. Before, the exit code was 101. - Each worker applies a drop of
wisp::uncache,wisp::revalidate_path,wisp::revalidate_tagorwisp::stale_tagonce. Before, a drop that came after a worker read the drop count, and before it read the list, ran again at its next check. - A raw control character (a byte below 0x20) in a JSON string is now an error:
expected a control character as an escape. RFC 8259 rejects it. This applies to package.json, cargo messages, NPM registry answers andwisp mcpclient messages. Before, Wisp accepted it. - With two
dependencieskeys in package.json, the NPM commands change the last one, which Wisp reads. Before, they changed the first one, and Wisp did not see the change. - An exact version follows SemVer 2.0.0. Wisp rejects an empty pre-release or build part (
1.2.3-,1.2.3+,1.2.3-a..b) and a leading 0 (01.2.3,1.2.3-01). Before, Wisp accepted them. wisp::uncachereads its prefix only as a path. Before, a prefix that started with a NUL or STX byte deleted the pages of a tag or marked them stale. A prefix that started with an SOH byte deleted only the page at that exact path.- Over HTTPS, a signed cookie with a
domain, or apathother than/, is__Secure-name, andsigned_cookiereads it back. Before, it kept its plain name andsigned_cookiereturnedNone. UnderWISP_BASEover HTTPS, a signed cookie is__Secure-name, notname.delete_cookiedeletes the signed name that default options give. - A client navigation waits for the head stylesheets of the new page that the shown page lacks. It waits 3 s at most, then shows the page. Before, the page showed without the sheet and shifted when the sheet loaded. A sheet that fails does not hold the navigation.
wisp build --static --spalists only the files that it creates (_redirects,vercel.json,404.html) for the next export to delete. Before, avercel.jsonor_redirectsof yours in the--outfolder got lines added, then the next export deleted it.wisp upgrade --undochecks Cargo.toml, Cargo.lock, AGENTS.md and the deploy workflow against the stamp of the upgrade, and names each file that changed. It refuses when the upgrade left no stamp. Wisp writes the stamp right after the bump, before the lock update. Before, a killed upgrade had no stamp, and the undo could overwrite later edits.wisp update-docs,wisp recipe,wisp upgrade,wisp deploy init,wisp openapi -o,wisp test --e2e --init, the NPM commands and the Cloudflare build write AGENTS.md, Cargo.toml,.gitignoreand the other files of your app whole (a new file, then a rename). Before, a crash or a full disk during the write could leave half a file.Upload::save_todoes the same when it copies to another disk.wisp buildwrites an Open Graph SVG over a file instatic/ogonly when the file ends with the mark of Wisp (<!-- wisp-og <hash> -->) and the hash still matches its text, with line ends ignored. It writes a PNG only when the file has atEXtchunkwisp-ogwith the hash of its other bytes. A PNG optimizer that removes that chunk makes the PNG yours: Wisp keeps it and shows a warning. Wisp writes each picture to a new file, then renames it. Before, a picture of yours with the same name was replaced.wisp upgradewrites asavedmarker last intarget/wisp-upgrade.wisp upgrade --undoand--force-undoput no file back without it, and delete only the files that it lists as absent. The refusal names the folder.- A write that stops in the middle (a crash) can leave a file
.<name>.<pid>-<n>.wisp-fixor.wisp-uploadbeside the target. The next write in that folder deletes such files older than one hour (a copy of an upload to another disk checks each folder at most once an hour, for up to 64 folders). New apps ignore both names in.gitignore. - A
Cargo.toml,.gitignore,wrangler.tomlor other file of your app that is a symbolic link now gives an error when a command writes it. Before, some commands wrote through the link. wisp::export: the warning for a guarded page says "A static file would be open to everyone".wisp lspshows noseo-title,seo-descriptionorseo-h1warning for a guarded page (const SIGNED_IN: bool = true;or aMIDDLEWAREconstant in the page, its+page.rsor a layout). The build already left these pages out. Before, the editor showed the warnings.src/hooks.rsrejects a constant with the name of a page knob only when it has the type of that knob (const TIMEOUT: u32).SIGNED_INfails with any type, as aconstor astatic. Before,const TIMEOUT: Durationfor the app's own code failed the build.- Templates: a tag name may hold a letter beyond ASCII (a custom element such as
<x-pluscafplus e with an acute accent), as HTML allows (the letters ofPCENChar). A component name stays ASCII. Before, the build stopped with "a tag's name ends with a blank,/or>". - Templates: a
{:js}hole skips a regular expression ({:s.replace(/}/g, '')}). A}in it does not end the hole. A regular expression longer than 256 bytes is a build error with a hint. - Netlify with Edge routes: the build warns for an Edge route that the excluded path of a Node route also takes (
a/bbeside the Node routea/[x]). Netlify has no rule order, so the Node function serves that route. wisp lsprename of a component reads a name in a Rust string over many lines, after a char literal with a quote ('"') and in a raw string. A name in a comment does not count. Before, it counted the quotes of one line.wisp lsprename of a block name also changes its uses in a browser text hole ({:#each xs as y}{:y}{:/each}). Only{:else},{:then},{:catch},{:await}and{:case}start a branch. Before, a text hole such as{:count}ended the scope, and the rename skipped the uses after it.wisp lsprename stops at a shorthand field after a string (${Foo { a: "x", w }}). Before, it changed the field name.wisp lsprename of a prop stops when a tag in another page already gives the new name. The build rejects such a tag with "<Card> getsheadingtwice". Before, the rename wrote the prop twice.wisp lsprename stops when a link to a folder is undersrc. The server does not follow the link, so it can miss a use there. Before, it skipped the link.wisp lspsignature help finds the parameters after the generics (pub fn run<F: Fn(u32)>(f: F, n: u32)). Go to definition of?/namefindspub(crate) fnand an action after an attribute with a]in a string.<form once>: a post that is dropped, times out or panics keeps its token busy for 60 s, so a resend cannot run the action twice. A post removes only its own token.- WebSocket:
recvcounts the idle ping as sent only after Wisp writes it. Before, arecvthat a channel message dropped while the ping waited to write could skip the ping. - Templates: a
confirmbutton with aclasson more than one line keeps the line numbers of the file. Before, the copy of the class in the summary added lines, and an error on a later line showed a number too high. - Templates:
<title description="..">with{@html ..}in the title builds. Before, Wisp copied the title into theog:titletag, and the build stopped with "{@html} is not allowed inside tags". - Build plugins: the list of outputs holds a hash of each output. The build deletes or replaces an output only when the file still holds the bytes that the build wrote. A changed file stops the build with an error that names it. Two outputs whose paths differ only by case also stop the build. Before, a stopped build (Ctrl-C, a panic) could leave a listed path that the build never wrote, and the next build could delete your file at that path. Two plugin names that differ only by case, such as
Upandup, now stop the build with an error, because Windows paths ignore case. - Plugins and layers: when
.wispis gone, a folder insrc/routesorsrc/componentswith the plugin mark counts as the build's own when its files equal the plugin's files. Before, the build stopped with an error on every build until you deleted the folder. wisp dev: a secondwisp devin the same app does not delete the executable copy of the first. Each run holds a lock in.wisp/run.wisp buildprerenders into.wisp/prerender-<pid>and deletes the folder when the second build ends, so two builds in one app do not delete each other's pages..wisp/scoped.cssis replaced, not rewritten, so the dev server never reads a half file.- Component libraries: the prefix
indexstops the build with an error. Its folder is the path of the index file.wisp/libs/index. - Templates: a
<title>with adescriptionattribute and a title or description on more than one line keeps the line numbers of the file, outside a${..}hole. Before, the copies in theog:titleandog:descriptiontags added lines, and an error on a later line showed a number too high. - Templates:
{:#await promise then value}splits at the lastthenoutside strings and brackets. Before, a string such as"a then b"in the promise ended the promise early. - The build deletes a plugin or layer folder in
src/routesorsrc/componentsonly when an earlier build copied it in. The build lists these folders in.wisp/plugin-folders. Before, it deleted any folder with the plugin mark in.gitignore. A folder that you copied from a plugin, such ascp -r "src/routes/(ui)" "src/routes/(myui)", was lost. A folder with the mark that no build listed now stops the build with an error. wisp route add <path> crudandwisp recipe addstop with an error whensrc/db.rsor.env.exampleexists and Wisp cannot read it (for example, bytes that are not UTF-8). Before, they replaced the file with only the new text.wisp fmtwrites each file to a new file and renames it. Before, it changed the file in place, and a stop or a full disk during the write could leave an empty template.- Cloudflare Workers and the Pages cron Worker: the build converts the weekday of a
wisp::cronschedule. Cloudflare counts 1 (Sunday) to 7, and Wisp counts 0 or 7 for Sunday. Before,0 9 * * 1-5ran Sunday to Thursday there, and0 3 * * 7ran on Saturday. The Worker maps each trigger back to the schedules that it stands for, so two schedules that convert to one trigger both run. - Deno: a weekday list with a
*item and a day item (*/2,1) converts the day item. Before, the list stayed as written, and Monday never ran.