Thomas Reggi's Profile Image

@thomasreggi 🌸

Declarative Server & Client Code to Enable Linting and Splitting.

October 18, 2022 (Syndicated From dev.to)

Cover Image

This is a proposal for an idea called “FileKinds”. It establishes literal eslinting rules for single files or groups of files. As well as optionally rules how clients and servers should utilize the code.

Part 1: The Purpose

Whats the point? Many JavaScript / TypeScript frameworks require specialized variables to be exported from files. For instance within Next.js we have getServerSideProps which has a specific type, within Deno Fresh we have handlers which has a specific type. Usually these files require the default export to also be a component type.

With the rise of meta-frameworks the union of server + client side code becoming increasingly popular. Frameworks like Next.js popularized named exports like getServerSideProps in a file that did specific things within the framework. It’s becoming apparent that the union of server + client is only going to become increasingly complex. Currently there are a couple ways to author split code:

  1. separate files like thing.client.js and thing.server.js
  2. one file where you have named exports and split using the AST server & client
  3. use a comment in the AST to define which should be split // @server & // @client
  4. a named export from a package say universal from @reggi/universal can be tracked by your AST parser of choice and you can pass a function into universal and split the code within the function argument

All of these solutions add complexity for how files / folders need to be authored and having clear rules around how to do that will be essential in getting this next wave of frameworks off the ground.

What about for groups of files? I always found it interesting within the mac ecosystem that the .app extension was applied to folders and abstracted out the contents of the folder with executability (when you click on it for instance). I like the idea of a contained component that has server functionality performing something like this as well. By giving the folder an extension a meta²-framework could pickup on how that component works without any configuration. For example for react server components we could use the rsc extension could use its own extension, etc.

about.rsc
  server.tsx
  client.tsx

Goals

  1. To provide linting for files / groups of files that have specific export signatures.
  2. To (optionally?) provide plugins on how a server should handle these files in a server framework like hattip

Provide the opportunity to then allow meta-frameworks to allow meta²-frameworks to render server + component code from any meta-framework. (allow a framework to potentially render out about.rsc and about.solid on the same server, potentially something like astro or another project could do this.

User Image
thomas
@thomasreggi
User Image
thomas
@thomasreggi

Example Deno Fresh Route

As an example I picked Deno fresh, because it uses named exports for routes. A route in Deno requires a default function to return that returns the component (in this case preact) and a handler object. With eslint it is possible to create a custom rule that would restrict what exports can be made from this FileKind, for instance it could give a linting error if the handler export was assigned a JSX.Element instead of an object like it requires. We could create a new FileKind called deno-fresh-route.

Example Deno Fresh Route
// routes/about.tsx

import { Handlers } from "$fresh/server.ts";

export const handler: Handlers = {
  async GET(req, ctx) {
    const resp = await ctx.render();
    resp.headers.set("X-Custom-Header", "Hello");
    return resp;
  },
};

export default function AboutPage() {
  return (
    <main>
      <h1>About</h1>
      <p>This is the about page.</p>
    </main>
  );
}

Part 2: Enforcement

When you have a custom FileKind like deno-fresh-route what can you do with it? How do you enforce / implement it? Here are two ways people should be able to use it.

Method one “Extensions”

When deno-fresh-route is registered a eslint package is included. I like the idea of FileKinds being able declare a sub-extension like about.fsh.ts

Method two “Rules”

Understandably library authors may not want to implement file sub-extensions. They may just want to state that all files within a given folder say routes are of a certain type. This should be configurable at the framework / editor level and not really seen by users. Unfortunately I can’t think of a better way of doing this then a filekinds.config.json at the route level something to tell the editor and linter which rules should run for which files / folders.

Method three “Comments”

Similar to how we currently juggle JSX runtimes you could use a comment at the top of a file to define its FileType.

Proposal filekinds.config.json
{
  "routes/*": "filekind.deno-fresh-route"
}

Part 3: The Organization

Part of this project is to create a standards body to claim specific file extension types for example about.rsc, along with a vscode extension that can detect these file types and bring them into your project automatically (by requesting access). Right now in order to achieve this level of linting every framework needs to implement their own vscode plugin. I think it would be really interesting if we could just have one that all frameworks could tap into.

Proposal Registry Definition for single file
{
  // name of the FileKind separate from the eslint-package-name
  "name": "deno-fresh-route",
  // extension of eslint rules with npm modules and options
  "rules": ["deno-fresh-route"],
  // reserved extension
  "extension": "{name}.fsh.ts"
}
Proposal Registry Definition for Folder
{
  // name of the FileKind separate from the eslint-package-name
  "name": "sveltekit-page",
  // extension of eslint rules with npm modules and options
  "folder": {
    "+page.svelte": {
      // possibly nothing not sure what to do with non js / ts files
      // if you really wanted you may need to use vite to compile then
      // run the rule checker
      "rules": ["svelte"]
    },
    "+page.js": {
      "rules": ["svelte-page-js"]
    },
    "+page.server.js": {
      "rules": ["svelte-page-server"]
    },
  // reserved extension
  "extension": "{name}.page.sveltekit"
}

Possible 🚩

  1. People don’t care about file / folder linting and the overhead
  2. People hate long file extensions like about.denofreshpreactroute.ts
  3. People hate abstract file extensions like about.dfpr.ts
  4. Versioning FileKinds
  5. Fights over good short useful extension names. (and the problems that come with governance)
  6. It’s not really feasible to include a multiple meta-frameworks (qwik, solidstart, sveltekit, next) in one framework.
  7. Including component + server code in one FileKind is not really desired overall as a practice.
  8. Open to other red-flags
  9. Security issues with the mono vscode extension
  10. It’s like babel all over again having to decide what code lives where is like syntax plugins 😵‍💫

Imagine a single file that can house web sockets action endpoints, tree-shaken client imports available from the server, and server endpoints. This is a Server FileKind that allows you define exports for different types of callable interactions. The AST tool would parse this file and shake out each part into server handlers using something like hattip inspired by this discussion here in brillout/vite-plugin-ssr.

export const socketActions = {
  hello () {
    // web socket action endpoint (like liveview)
    console.log('hello on server');
    return { hello: 'true'}
  }
}

export const clientImports = {
  hello () {
    // this is a chunk of js that will be shaken out of this file (with it's external scopes if any?)
    // imported into the client not ajax
    // this isn't loaded at runtime and is loaded on a dom event (like qwick)
    console.log('hello on client');
    return { hello: 'true'}
  }
}

export const serverEndpoints = {
  hello () {
    // this is a server endpoint (like telefunc)
    console.log('hello on server');
    return { hello: 'true'}
  }
}

Argument shaking example:

A different example of static server generation not from exports but from a keyword from a specific module universal declarations would compose the server endpoints. I did this in an astro file which would make things extra hard, but the idea is there. Uses web-component to provide globals.

---
import { staticallyParsedRules, universal } from '@reggi/example'

universal(() => ({
  thing: {
    server: () => {
      console.log('hi from server')
    },
    client: () => {
      console.log('hi from client')
    },
    socket: () => {
      console.log('hi from server socket')
    }
  }
}))

---

<dom-global-rules rules={staticallyParsedRules}></dom-global-rules>
<div onclick="thing.server()">log something on the server</div>
<div onclick="thing.socket()">log something on the socket</div>
<div onclick="thing.client()">log something on the client</div>