Written By: Astrid Gealer astrid@webscalesoftware.ltd
In ECMAScript based platforms, it is needlessly complex and non-standardised to do the following:
- Build platforms that consume multiple frameworks. Each framework has their own ways of doing things, and there's no standard way of figuring out where everything is. This means you need to implement a bunch of specific adapters for different frameworks.
- Go ahead and put a application on another path that's using another framework. This generally requires complex platform-specific rewriting solutions or multiple ports being created with many web server rules.
- Write your own web server to serve ECMAScript applications and the public content with them.
This aims to solve this by creating a standardised interface with a not dissimilar goal to Rack, except also including a reference to the public folder to allow for the serving of assets easily and the build script to allow for the web server/CI pipeline to direct a build before it loads.
When a Servefile is loaded, all string substitutions must be resolved. You MUST error if this is not possible.
A Servefile MUST be either:
-
A YAML file with or without 3 dashes on a new line with a use directive. If there are dashes, the only content after it MUST be new lines since the specified file will contain the Servefile information.
-
NOT a use directive and the following separated by 3 dashes on a new line:
- The standard YAML structure MUST be the first item.
- The ECMAScript implementation MUST be the second item.
No other 3-dash split items are permitted and you MUST error if more are specified.
The default location for a Servefile is at the root of the project.
In a Servefile, you can use ${{ ENV_VAR_HERE }} (where ENV_VAR_HERE is the environment variable) to reference an environment variable inside any string or anywhere where it could become a string. The spacing is not
In all YAML objects (including the root) defined in this specification, an environment block can be used to split out different implementations for different ECMAScript engines. Any keys MUST not be used in this implementations objects. By default, it is presumed you are on a Node compatible environment. These objects MUST NOT be recursive, however the use directive would count as another scope, so the loaded file can use directives too.
In the event a block is matched, it MUST replace all the keys specified inside of it with the keys in the parent and error if it violates the specification. If multiple blocks are matched, the most specific block SHOULD be used.
All serverless environments that are not Node compatible SHOULD obey the serverless block. Additionally, running in development mode should cause the development block to be obeyed. All other blocks are up to the environment.
For environments that are identical across different frameworks, it might make sense to have a framework specific file somewhere and reference it from projects. For this, we have the use directive. If you are going to use this, your YAML MUST be a object with the following structure:
version(1 | "1"): This MUST be set to 1 to follow this specification.use(string | UseOptions): Either the file path OR a object with the following:path: The file path for the Servefile.env(Optional,{[envName: string]: string} | null | undefined): If provided, this will be merged into the current environment variables BEFORE the path is imported.
All Servefile's that are imported can support later versions of the specification if the server supports them. If not, it MUST error. Any imported files that are version 1 MUST follow the V1 file structure.
This directive supports environment blocks. If any other invalid keys or keys that do not follow the environment part of this specification are found, you MUST error.
If there is a file recursion loop, your implementation MUST error.
The file path MUST be a string and one of the following:
- A relative path: The path is resolved relative to the Servefile in question.
- A absolute path: This should resolve to the file at the path on the file system.
node_modulesscanning: If the path is neither of these, it MUST be split by/. We then do the following:- If the length after split is 1, then look for
Servefilein the module. - We recursively go through each
node_modulesfolder, starting at any inside the folder with theServefile. If we don't find the module, we go to the parent and look for anode_modulesfolder there until we have exhausted all options for this. If we find a folder matching the module name, we do the following until one of them finds a result:- We try to resolve the rest of the split relative to
distDirif its present. - We try to resolve the rest of the split relative to the root of the module.
- We try to resolve the rest of the split relative to
- If the length after split is 1, then look for
If the file is unable to resolve, your implementation MUST error.
This must be a YAML object of the following structure:
version(1 | "1"): This MUST be set to 1 to follow this specification.cjs(optional,boolean | null | undefined): Defines if this Servefile should be treated as CJS. In non-bundled environments, this SHOULD also change the behariour of the runtime unless the runtime supports both anyway. If this is unset or falsey, assume ESM.build(optional,string | null | undefined):: The command to run before anything else. Note that bothdev_buildandproduction_buildoverride this. This command MUST be ran before resolving paths.dev_build(optional,string | null | undefined): The command to run before anything else in development mode. This command MUST be ran before resolving paths.production_build(optional,string | null | undefined): The command to run before anything else in production. This command MUST be ran before resolving paths.public_folder(optional,string | null | undefined): The public folder to serve content for relative to the path this is mounted at (or an absolute path). Note that bothdev_public_folderandproduction_public_folderoverride this.dev_public_folder(optional,string | null | undefined): The public folder used to serve content for development relative to the path this is mounted at (or an absolute path).production_public_folder(optional,string | null | undefined): The public folder used to serve content for production relative to the path this is mounted at (or an absolute path).dev_watch(optional,string[] | false | null | undefined): A list of paths to watch in development. If this is empty or unset, it is up to the server to decide. When the watch is triggered, the worker MUST be killed and recreated without stopping the main server. If this is set tofalse, the server MUST NOT watch for changes to any paths.js_output_path(optional,string | null | undefined): The JS output path. If this is unset, the JS in the Servefile is ran from the initially imported Servefile's path.
This directive supports environment blocks. If any other invalid keys or keys that do not follow the environment part of this specification are found, you MUST error.
Note this implementation MUST NOT be in TypeScript. The purpose of this file is not to enforce opinions about typing, but rather to include the smallest amount of code to bootstrap the request interface.
The JS chunk of the implementation MUST be scanned for any of the environment variable names being included in this anywhere. If any of them matches, they MUST be merged into the environment before the bootstrapping JS is loaded.
All imports in this function are relative to the js_output_path value if set, and if not relative to the initially imported Servefile.
The signature of the bootstrapping function MUST be () => Promise<(rootPath: string, request: Request) => Promise<Response>> (but without the types). The way this will be called is the following:
-
On the start of the worker, the async function will be called once and the returned function by this will be what is called every request. Once the promise is resolved successfully, the worker is classified as ready. If the promise rejects, you MUST error.
-
When there is a request, the async server function will be called with the following:
rootPath(string): The root of the start of where this application is mounted. This is generally/, but If it is mounted from/subpathfor example, that is what this will be. You can substitute this from the request path to get the path for routing within your applciation.request(Request): A web standard request object.
The return result from a request MUST be a web standard Response object. In the event this isn't the case or the promise rejects, your server MUST error and the worker MUST be considered dead.
If cjs was set to true, the bootstrapping function should be exported with module.exports = <init function>. If not, you should export it with export default <init function>.