I wrote a mild response to this on my blog: On vinext. Excerpts follow. Please do not take the length of this comment as its authoritativeness. In fact, the opposite is probably wise. Going into...
Exemplary
I wrote a mild response to this on my blog: On vinext. Excerpts follow.
Please do not take the length of this comment as its authoritativeness. In fact, the opposite is probably wise.
Going into this, I thought state-of-the-art AI models were generally quite good. I've heard many success stories, but I've also tried all the major models' free tiers and they haven't been much of a help---[^1]although I can already feel myself getting sucked into relying on AIs to solve problems.
Let's review the code! I only have time for one file, so let's start with packages/vinext/src/server/prod-server.ts, which states:
/** * Production server for vinext. * * Serves the built output from `vinext build`. Handles: * [...] */
That sounds pretty fundamental to me, so let's dive in.
The file starts out with the definition of a function, readNodeString:
/** Convert a Node.js IncomingMessage into a ReadableStream for Web Request body. */functionreadNodeStream(req: IncomingMessage):ReadableStream<Uint8Array>{
Now, I didn't know this, but those of you who know NodeJS may have realized that IncomingMesssage extends NodeJS's stream.Readable, which has a toWeb method to do this already. Why didn't vinext just use this instead, you may ask? Well, they do use it. In the same file.
Moving on, next, prod-server.ts defines an options interface.
exportinterfaceProdServerOptions{/** Port to listen on */port?: number;/** Host to bind to */host?: string;/** Path to the build output directory */outDir?: string;/** Disable compression (default: false) */noCompression?: boolean;}
There are two really weird choices I see: negating a boolean like noCompression (I'd prefer the positive compress?: boolean), and why is everything optional? One potential corollary of the parse-don't-validate mindset is doing, uh, anything but what vinext has done here.
[But this feels pretty user-facing, so this may just be a holdover from NextJS.] [...]
[...]
Moving on, up next we have some utility functions to compress a response:
/** Content types that benefit from compression. */constCOMPRESSIBLE_TYPES=newSet(["text/html",// [...]"application/wasm",]);/** Minimum size threshold for compression (in bytes). Below this, compression overhead isn't worth it. */constCOMPRESS_THRESHOLD=1024;functioncreateCompressor(encoding:"br"|"gzip"|"deflate"):zlib.BrotliCompress|zlib.Gzip|zlib.Deflate{// [...]}functionsendCompressed(req: IncomingMessage,// [...]compress: boolean=true,):void{// [...]}
This seems completely fine to me. Then we have this:
/** Content-type lookup for static assets. */constCONTENT_TYPES: Record<string,string>={".js":"application/javascript",".mjs":"application/javascript",".css":"text/css",".html":"text/html",".json":"application/json",".png":"image/png",".jpg":"image/jpeg",".jpeg":"image/jpeg",".gif":"image/gif",".svg":"image/svg+xml",".ico":"image/x-icon",".woff":"font/woff",".woff2":"font/woff2",".ttf":"font/ttf",".eot":"application/vnd.ms-fontobject",".webp":"image/webp",".avif":"image/avif",".map":"application/json",};
This is extremely not fine to me. It has .eot, an (apparently) font format I've never heard of, while leaving out:
all audio formats
all video formats
some niche image formats (like .pdf)
WebAssembly files, which will at least be compressed before being sent as application/octet-stream
And then we have tryServeStatic, which looks like this:
/** * Try to serve a static file from the client build directory. * Returns true if the file was served, false otherwise. */functiontryServeStatic(req: IncomingMessage,res: ServerResponse,clientDir: string,pathname: string,compress: boolean,):boolean{
Okay, compress isn't optional anymore, weird. And I'd like to see some Apps Hungarian on clientDir to note it's already been path.resolve()d, as it is in every codepath that gets here. Because it wasn't clearly marked, they're going to immediately resolve it again ([tag]s mine):
// Resolve the path and guard against directory traversal (e.g. /../../../etc/passwd)constresolvedClient=[1]path.resolve(clientDir);letdecodedPathname: string;try{decodedPathname=[2]decodeURIComponent(pathname);}catch{returnfalse;}conststaticFile=[3]path.resolve(clientDir,"."+decodedPathname);if(!staticFile.startsWith(resolvedClient+path.sep)&&staticFile!==resolvedClient){[4]returnfalse;}if([5]pathname==="/"||!¯fs.existsSync(staticFile)||!fs.statSync(staticFile).isFile()){returnfalse;}
First off, they're resolving clientDir twice? [1] [3]; and then this try/catch is really scary to me. I definitely would try to split this up into a few separate functions. An incomplete sketch follows:
getRequestPathname, which takes an IncomingMessage, calls decodeURIComponent [2], and sanitizes the path.
Sanitizing here should fail if the path contains any characters that any platforms consider special, like :\?, and additionally locally resolve ..s (but without access to the filesystem, like path.resolve would.) This would change behavior. Current vinext will bail out the instant it touches a symlink, but resolving the path this early means we don't know where we'll end up looking.
I'd argue that following symlinks is correct when you're serving static files, but I'm not Cloudflare, either.
resolveToStaticFile which takes a path and the static file directory and returns some sort of byte stream,
serveStream which takes a content type and a stream, heuristically compress the response, and send the response body.
At [4] they're just checking that this file is within the static file root, which is at clientDir. Calling startsWith and then !== is totally fine here---if the static file root is /var/www/my-site, we don't want to allow /var/www/my-site-2/foo/bar (which would resolve from a pathname of "../my-site-2".) The worse part is that this should be its own function, as this pattern is used again later on in the same file, and this kind of function feels like it might have nonobvious edge cases.
And then we get to the really big issue: [5] we're calling synchronous functions! These will block the event loop in Node.JS, and the only way to get around this is to run a fleet of microservers at the edge that will automatically scale up and down based on load. Running at the edge will also reduce latency, so your customers will be happier. (For the record, this is a joke---I do not believe Cloudflare is writing worse code to make you start using Cloudflare-or-equivalent services.)
Thankfully we're only running existsSync and statSync. These probably won't take very long to complete. And while we're on the topic, another minor nit is that I'd prefer to skip existsSync and catch the potential error from statSync. Less syscalls is always good.
[...]
Personally, I would not run this server in production. (To be fair, I'd say the same about NextJS for entirely unrelated reasons.)
There is a lot of code here, and it seems like everywhere I look, I see duplicate and/or wasted code. I am fairly confident the rest of the codebase is similar; one other thing that stuck out to me when I was skimming was a regular expression parser to lint against malicious double-repeats. Surely this doesn't belong in a web server?
The texture of the code is utterly alien to me, and I no longer think state-of-the-art AI models are good.
It's actually little baffling that these issues are here. Every issue I pointed out is trivial to fix, even by LLM standards; I'm not sure anyone has looked at this code.
From the article: [...] [...] [...] [...] [...] [...] Also, a large company can probably clone your open source project rather easily. And so could a hobbyist.
From the article:
Last week, one engineer and an AI model rebuilt the most popular front-end framework from scratch. The result, vinext (pronounced "vee-next"), is a drop-in replacement for Next.js, built on Vite, that deploys to Cloudflare Workers with a single command. In early benchmarks, it builds production apps up to 4x faster and produces client bundles up to 57% smaller. And we already have customers running it in production.
The whole thing cost about $1,100 in tokens.
[...]
The current deployment target is Cloudflare Workers, but that's a small part of the picture. Something like 95% of vinext is pure Vite. The routing, the module shims, the SSR pipeline, the RSC integration: none of it is Cloudflare-specific.
Cloudflare is looking to work with other hosting providers about adopting this toolchain for their customers (the lift is minimal — we got a proof-of-concept working on Vercel in less than 30 minutes!). This is an open-source project, and for its long term success, we believe it’s important we work with partners across the ecosystem to ensure ongoing investment. PRs from other platforms are welcome. If you're interested in adding a deployment target, open an issue or reach out.
[...]
We want to be clear: vinext is experimental. It's not even one week old, and it has not yet been battle-tested with any meaningful traffic at scale. If you're evaluating it for a production application, proceed with appropriate caution.
[...]
A project like this would normally take a team of engineers months, if not years. Several teams at various companies have attempted it, and the scope is just enormous. We tried once at Cloudflare! Two routers, 33+ module shims, server rendering pipelines, RSC streaming, file-system routing, middleware, caching, static export. There's a reason nobody has pulled it off.
This time we did it in under a week. One engineer (technically engineering manager) directing AI.
[...]
What changed from those earlier attempts? AI got better. Way better.
[...]
Why do we have so many layers in the stack? This project forced me to think deeply about this question. And to consider how AI impacts the answer.
[...]
It's not clear yet which abstractions are truly foundational and which ones were just crutches for human cognition. That line is going to shift a lot over the next few years. But vinext is a data point. We took an API contract, a build tool, and an AI model, and the AI wrote everything in between. No intermediate framework needed. We think this pattern will repeat across a lot of software. The layers we've built up over the years aren't all going to make it.
Also, a large company can probably clone your open source project rather easily. And so could a hobbyist.
I wrote a mild response to this on my blog: On vinext. Excerpts follow.
Please do not take the length of this comment as its authoritativeness. In fact, the opposite is probably wise.
Going into this, I thought state-of-the-art AI models were generally quite good. I've heard many success stories, but I've also tried all the major models' free tiers and they haven't been much of a help---[^1]although I can already feel myself getting sucked into relying on AIs to solve problems.
Let's review the code! I only have time for one file, so let's start with
packages/vinext/src/server/prod-server.ts, which states:That sounds pretty fundamental to me, so let's dive in.
The file starts out with the definition of a function,
readNodeString:Now, I didn't know this, but those of you who know NodeJS may have realized that
IncomingMesssageextends NodeJS'sstream.Readable, which has atoWebmethod to do this already. Why didn't vinext just use this instead, you may ask? Well, they do use it. In the same file.Moving on, next,
prod-server.tsdefines an options interface.There are two really weird choices I see: negating a boolean like
noCompression(I'd prefer the positivecompress?: boolean), and why is everything optional? One potential corollary of the parse-don't-validate mindset is doing, uh, anything but what vinext has done here.[But this feels pretty user-facing, so this may just be a holdover from NextJS.] [...]
[...]
Moving on, up next we have some utility functions to compress a response:
This seems completely fine to me. Then we have this:
This is extremely not fine to me. It has
.eot, an (apparently) font format I've never heard of, while leaving out:.pdf)application/octet-streamAnd then we have
tryServeStatic, which looks like this:Okay,
compressisn't optional anymore, weird. And I'd like to see some Apps Hungarian onclientDirto note it's already beenpath.resolve()d, as it is in every codepath that gets here. Because it wasn't clearly marked, they're going to immediately resolve it again ([tag]s mine):First off, they're resolving
clientDirtwice? [1] [3]; and then this try/catch is really scary to me. I definitely would try to split this up into a few separate functions. An incomplete sketch follows:getRequestPathname, which takes anIncomingMessage, callsdecodeURIComponent[2], and sanitizes the path.Sanitizing here should fail if the path contains any characters that any platforms consider special, like
:\?, and additionally locally resolve..s (but without access to the filesystem, likepath.resolvewould.) This would change behavior. Currentvinextwill bail out the instant it touches a symlink, but resolving the path this early means we don't know where we'll end up looking.I'd argue that following symlinks is correct when you're serving static files, but I'm not Cloudflare, either.
resolveToStaticFilewhich takes a path and the static file directory and returns some sort of byte stream,serveStreamwhich takes a content type and a stream, heuristically compress the response, and send the response body.At [4] they're just checking that this file is within the static file root, which is at
clientDir. Calling startsWith and then!==is totally fine here---if the static file root is/var/www/my-site, we don't want to allow/var/www/my-site-2/foo/bar(which would resolve from apathnameof"../my-site-2".) The worse part is that this should be its own function, as this pattern is used again later on in the same file, and this kind of function feels like it might have nonobvious edge cases.And then we get to the really big issue: [5] we're calling synchronous functions! These will block the event loop in Node.JS, and the only way to get around this is to run a fleet of microservers at the edge that will automatically scale up and down based on load. Running at the edge will also reduce latency, so your customers will be happier. (For the record, this is a joke---I do not believe Cloudflare is writing worse code to make you start using Cloudflare-or-equivalent services.)
Thankfully we're only running
existsSyncandstatSync. These probably won't take very long to complete. And while we're on the topic, another minor nit is that I'd prefer to skipexistsSyncand catch the potential error fromstatSync. Less syscalls is always good.[...]
Personally, I would not run this server in production. (To be fair, I'd say the same about NextJS for entirely unrelated reasons.)
There is a lot of code here, and it seems like everywhere I look, I see duplicate and/or wasted code. I am fairly confident the rest of the codebase is similar; one other thing that stuck out to me when I was skimming was a regular expression parser to lint against malicious double-repeats. Surely this doesn't belong in a web server?
The texture of the code is utterly alien to me, and I no longer think state-of-the-art AI models are good.
It's actually little baffling that these issues are here. Every issue I pointed out is trivial to fix, even by LLM standards; I'm not sure anyone has looked at this code.
[^1]: I wrote this emdash. Trust me or don't.
From the article:
[...]
[...]
[...]
[...]
[...]
[...]
Also, a large company can probably clone your open source project rather easily. And so could a hobbyist.