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:
- separate files like
thing.client.js
andthing.server.js
- one file where you have named exports and split using the AST
server
&client
- use a comment in the AST to define which should be split
// @server
&// @client
- 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
- To provide linting for files / groups of files that have specific export signatures.
- 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.
thomas 🌻@thomasreggiWith this proposal we’d could something like:hello-world.wc.ts
And all tools / frameworks would be able to know what the wc.ts format is and how to expect it.
22:36 PM – 18 Oct 2022
thomas 🌻
@thomasreggiDeclarative Server & Client Code to Enable Linting and Splitting. https://t.co/txtRul2vz9
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 (
<h1>Abouth1>
<p>This is the about page.p>
);
}
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 🚩
- People don’t care about file / folder linting and the overhead
- People hate long file extensions like
about.denofreshpreactroute.ts
- People hate abstract file extensions like
about.dfpr.ts
- Versioning FileKinds
- Fights over good short useful extension names. (and the problems that come with governance)
- It’s not really feasible to include a multiple meta-frameworks (
qwik
,solidstart
,sveltekit
,next
) in one framework. - Including component + server code in one FileKind is not really desired overall as a practice.
- Open to other red-flags
- Security issues with the mono vscode extension
- 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>