Yesterday (15 Oct 2024), I used Cursor to code more than I ever have. (Doing’s how we learn, I guess. Not just reading.)
This was mainly to create and publish 2 libraries on npm over 6 hours:
- asyncsse – which converts a Server-Sent Event stream into an async iterator that I can use in a for await … of loop
- asyncllm – which standardizes the Server-Sent Events streamed by the popular LLMs into an easy to use form.
This exercise broke several mental barriers for me.
Writing in a new language. Deno 2.0 was released recently. I was impressed by the compatibility with npm packages. Plus, it’s a single EXE download that includes a linter, tester, formatter, etc. Like all recent cool fast tools, it’s written in Rust. So I decided to use it for testing. Running deno test
runs the entire test suite. My prompts included asking it to:
Create a Deno HTTP server to mock requests for the tests
. This is cool because a single, simple code chunk runs the server within the test suite.Serve static files from samples/
to move my tests into files
Writing test cases. Every line of this code was written by Cursor via Claude 3.5 Sonnet. Every line. My prompt was, Look at the code in @index.js and write test cases for scenarios not yet covered.
It’s surprising how much of the SSE spec it already knew, and anticipated edge cases like:
- SSE values might have a colon. I learnt for the first time that the
limit
parameter in String.split() is very different from Python’s str.split. (The splits, then picks the first few, ignoring the rest. Python ensures the rest is packed into the last split.) This helped me find a major bug. - SSE has comments. Empty keys are treated as strings. Didn’t know this.
I was able to use it to generate test cases based on content as well. Based on @index.js and @openai.txt write a test case that verifies the functionality
created the entire test case for OpenAI responses. (I did have to edit it because LLMs don’t count very well, but it was minimal.)
Bridging test coverage gaps. The prompt that gave me the most delightful result was Are there any scenarios in @index.js not tested by @test.js?
It did a great job of highlighting that I hadn’t covered Groq, Azure, or CloudFlare AI workers (though they were mentioned in the comments), error handling, empty/null values in some cases, tested for multiple tool calls. I had it generate mock test data for some of these and added the tests.
Enhancing knowledge with references. I passed Cursor the SSE documentation via @https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events
and asked it to find more scenarios my code at @index.js
had not covered. This found a number of new issues.
- SSE values can be split across multiple lines.
- Fields without values are valid.
- Empty lines should be ignored.
Generating bindings. I avoid TypeScript because I don’t know it. Plus, it requires an compilation step for the browser. But TypeScript bindings are helpful. So I prompted Cursor, using the Composer (which can create new files) to Create TypeScript bindings for @index.js in index.d.ts
– which id did almost perfectly.
Check for errors. I typed Check this file for errors
on @index.d.ts. I don’t know enough to figure this out. It went through the description and said everything seems fine. But I saw a TypeScript plugin error that said, Property 'data' of type 'string | undefined' is not assignable to 'string' index type 'string'.ts(2411)
. When prompted, it spotted the issue. (The earlier code assumed all properties are strings. But some can be undefined too. It fixed it.)
Documentation. At first, I asked the Composer to Create a README.md suitable for a world-class professional open source npm package
and it did a pretty good job. I just needed to update the repository name. I further prompted it to Modify README based on @index.js and share examples from @test.js
on asyncllm, which did an excellent job.
Code review. I asked it to Review this code. Suggest possible improvements for simplicity, future-proofing, robustness, and efficiency
and it shared a few very effective improvements.
- Regex lookaheads for efficient regular expression splitting, i.e. use
buffer.split(/(?=\r?\n\r?\n)/)
instead ofbuffer.split(/(\r?\n\r?\n)/)
— and though I haven’t tested this, it looked cool. - Restructuring complex if-else code into elegant parsers that made my code a lot more modular.
- Error handling. It added
try {} catch {}
blocks at a few places that helped catch errors that I don’t anticipate but don’t hurt.
Code simplification. Several times, I passed it a code snippet, saying just Simplify
. Here’s an example:
const events = [];
for await (const event of asyncLLM(...)) {
events.push(event);
}
This can be simplified to
const events = await Array.fromAsync(asyncLLM(...))
Packaging. I copied a package.json
from an earlier file and asked it to Modify package.json, notable keywords and files and scripts based on @index.js
which it did a perfect job of.
Blogging. I wrote this blog post with the help of the chat history on Cursor. Normally, such blog posts take me 3-4 hours. This one took 45 minutes. I just had to pick and choose from history. (I lost a few because I renamed directories. I’ll be careful not to do that going forward.)
Overall, it was a day of great learning. Not in the classroom sense of “Here’s something I didn’t know before”, but rather the cycling / swimming sense of “Here’s something I now know to do.”