Deno is a modern runtime for JavaScript and TypeScript that simplifies the process of building applications. Currently,
I’m working on a tool for building full-stack applications with Deno. Applications built with this tool have two entry
points: src/backend/main.ts and src/frontend/main.ts. The backend is a standard Deno app, while the frontend needs
to go through esbuild first to generate two bundle.js files – one for server-side rendering and one for client-side
rendering.
Although one bundle could suffice, the additional magic happening in the process simplifies API implementations, making my life easier. But that’s not the focus for now!
In the frontend, it should be possible to import CSS files using "import," which then end up in the bundle.css. While
earlier versions of Deno made this challenging, importing CSS files is now relatively straightforward. **Unfortunately,
** deno check still gets upset if you try to import classNames from .css files, as Deno doesn’t know what to do with
this.
Here’s why deno check is problematic:
- CSS Imports: While importing CSS files is supported, importing specific classNames from them is not.
- Interference: It interferes with the seamless development experience, showing errors that disrupt productivity.
To tackle this, I created a tool that searches for all .css files in the frontend and generates .d.ts files for
them. Then it uses an importMap to overwrite importing the .css with the newly created .d.ts files – thus appeasing
Deno.
This method works great! However, deno lsp now suggests that all .css file imports should be adjusted as found in
the importMap, which is quite frustrating since it's not what I want.
As ignoring "import-map-remap" via a comment is impossible, I decided to write a wrapper for deno lsp.
This wrapper is quite simple:
- Initialization: When started, it simply starts the Deno LSP and pipes stdin, stdout, and stderr back and forth.
- Manipulation: It manipulates stdout by checking if a diagnostic of "import-map-remap" is encountered. If it is a relative import, or if the current import is shorter than the suggestion from Deno, the diagnostic is filtered out.
We'll break down the crucial parts of the code for clarity.
Here's how we initialize and spawn the Deno LSP process:
import { dirname, join } from "https://deno.land/std@0.224.0/path/mod.ts";
const handleLspCommand = async () => {
const process = (
new Deno.Command(join(dirname(Deno.execPath()), "./deno"), {
env: Deno.env.toObject(),
cwd: Deno.cwd(),
args: Deno.args,
stdin: "piped",
stdout: "piped",
stderr: "piped",
}).spawn()
);
// ... Other initialization code
};This part of the TransformStream handles expanding the buffer if needed and extracting the content length from
headers:
const textDecoder = new TextDecoder();
const textEncoder = new TextEncoder();
let contentLength;
let buffer = new Uint8Array(1024);
let bufferCursor = 0;
const handleStream = new TransformStream({
transform(chunk, controller) {
if (buffer.length < bufferCursor + chunk.length) {
const newBuffer = new Uint8Array(buffer.length * 2);
newBuffer.set(buffer, 0);
buffer = newBuffer;
}
buffer.set(chunk, bufferCursor);
bufferCursor += chunk.length;
for (; ;) {
// Process the buffer and parse headers to find content length
if (contentLength === undefined) {
const index = buffer.findIndex((_, index, buffer) => {
return (
buffer[index + 0] === 13 && // '\r'
buffer[index + 1] === 10 && // '\n'
buffer[index + 2] === 13 && // '\r'
buffer[index + 3] === 10 // '\n'
);
});
if (index !== -1) {
const headersText = textDecoder.decode(buffer.slice(0, index));
buffer.set(buffer.slice(index + 4), 0);
bufferCursor -= index + 4;
buffer.fill(0, bufferCursor);
const headers = new Headers(headersText.split("\r\n").map(header => {
const splitIndex = header.indexOf(":");
return [
header.slice(0, splitIndex).trim(),
header.slice(splitIndex + 1).trim(),
];
}));
const contentLengthText = headers.get("content-length");
if (!contentLengthText) {
Deno.exit(1); // Exit if content-length header is missing
}
contentLength = +contentLengthText; // Parse content length
continue; // Continue to next iteration
}
}
// ... Additional processing
break;
}
},
// ... Other required handlers
});Once the content length is known and the content is ready, we process and filter diagnostics:
for (; ;) {
// ... Additional processing
if (contentLength <= bufferCursor) {
const contentText = textDecoder.decode(buffer.slice(0, contentLength));
buffer.set(buffer.slice(contentLength), 0); // Remove processed data from buffer
bufferCursor -= contentLength; // Update cursor position
buffer.fill(0, bufferCursor, buffer.length); // Clear the remaining part of buffer
contentLength = undefined; // Reset content length
const contentJson = JSON.parse(contentText);
// Filter out unnecessary diagnostics if method is 'textDocument/publishDiagnostics'
if (contentJson.method === "textDocument/publishDiagnostics") {
contentJson.params.diagnostics = contentJson.params.diagnostics.filter(diagnostic => !(
diagnostic.code === "import-map-remap" &&
diagnostic.data.from.startsWith("./") &&
diagnostic.data.from.length < diagnostic.data.to.length
));
}
const contentBytes = textEncoder.encode(JSON.stringify(contentJson));
controller.enqueue(textEncoder.encode(`Content-Length: ${contentBytes.length}\r\n\r\n`));
controller.enqueue(contentBytes);
continue; // Continue to next iteration
}
break;
}We also need to handle the types command to ensure everything works smoothly with WebStorm:
const handleTypesCommand = async () => Deno.exit(await (
new Deno.Command(join(dirname(Deno.execPath()), "./deno"), {
env: Deno.env.toObject(),
cwd: Deno.cwd(),
args: Deno.args,
stdin: "inherit",
stdout: "inherit",
stderr: "inherit",
}).output().then(output => output.code)
));
switch (Deno.args[0]) {
case "lsp":
await handleLspCommand();
break;
case "types":
await handleTypesCommand();
break;
}
Deno.exit(1);You can find the complete source code on GitHub Gist.
After modifying the path to deno (lsp) in my IDE, things worked with the exception that WebStorm could no longer load
type definitions. It was quite annoying to figure out why, but the solution required not just wrapping the lsp
sub-command of Deno but also the types sub-command – since WebStorm’s Deno plugin uses both commands.
Once I wrapped both sub-commands, everything worked perfectly, and I could patch type definitions and diagnostics
from deno lsp.
- Seamless Development: By filtering out unnecessary diagnostics, the development experience becomes smoother.
- Maintainability: Avoided the need to fork and patch Deno itself, which would have been more cumbersome.
Working on this solution provided valuable insights into the inner workings of Deno and its LSP. While not an optimal solution, it showcases the flexibility and power of creating custom wrappers to meet specific needs. My advice to others facing similar challenges is to explore creative solutions and always test thoroughly in different environments.
Feel free to incorporate these insights and improvements into your workflow for a more productive and less frustrating development experience!