<onWebFocus />

Knowledge is only real when shared.

Introducing zero-configuration

May 10, 2024

A more generic approach to handling all of your configuration needs.

While I've already presented the reasoning for why I prefer a zero-configuration approach in this Project Configuration post, recent breaking configuration changes to ESLint have made it necessary to reconsider the previous implementation approach. Starting with ESLint 9, a configuration file needs to be present in the root of the project, and extending one from your package.json will no longer work. Additionally, one of my favorite successors to ESLint, Biome, requires a configuration file to be present. From what I can tell, there hasn't really been a standard forming around how plugins should be configured, and each tool requires its own file. This new tool aims to create such a simple standard.

Previous Approach

Previously, I built three separate tools: papua for Frontend, padua for plugins, and squakfor backend. All three share mostly the same logic to achieve their respective functionalities, but abstracting it back then seemed overkill. Of course, when starting out, I didn't anticipate that I would favor this approach so much that I would build multiple tools using it. The commonalities between the first and second approach didn't seem significant enough to justify the overhead of creating a generic abstraction. All these tools come fully featured with an opinionated choice of tools; papua uses Rspack for bundling, ESLint for linting, Prettier for formatting, and Jest or Vitest for testing, while padua uses esbuild to bundle plugin code.

Introducing zero-configuration

zero-configuration logoNow, with breaking changes in ESLint 9, all plugins would need to be updated. As is typical in open-source development, many other features in these plugins have become outdated, prompting me to question whether there is a fundamentally better approach. The new plugin will only handle the configuration part, but in a much more flexible and improved manner. To provide the same functionality as the previous plugins, while being more generic, the new approach uses templates (which, albeit containing more boilerplate) to accommodate many different use cases.

The Problem Being Solved

Few developers even mind the presence of tons of configuration files in the root of the project, and adding another one often feels like adding a sticker to your laptop to show allegiance to your favorite technologies. However, this clashes with the fundamental programming principle of simplicity. While this plugin isn't built to discourage adding in fewer plugins, it's meant to maximize meaningful code in your repository. When you look at a project, the last thing you want to check out is the configuration files. Additionally, you don't want to deal with generated code or boilerplate code, as these don't really count as source code that contains any value. Furthermore, repeating the same configurations between different tools should be avoided.

How It Works

Luckily, most plugins and frameworks have already embraced a zero-configuration approach where nothing needs to be configured to cover the basic use cases. However, the existence of the configuration file itself or small customizations are often necessary. This plugin will pull configurations from only two locations: the configuration property in your package.jsonor a dedicated configuration.ts file.

{
  "name": "my-application",
  "scripts": {
    "check": "biome check --apply ."
  },
  "dependencies": {
    "@biomejs/biome": "^1.7.3",
    "zero-configuration": "^0.7.2"
  },
  "trustedDependencies": [
    "zero-configuration"
  ],
  "configuration": {
    "biome": "recommended"
  }
}

The above demonstrates a package.json configured to handle Biome formatting and linting without the need to add the required biome.json file to the source. Extending therecommended defaults provided by zero-configuration will get you started right away with sensible defaults.

import { defineConfig, devices } from '@playwright/test'
      
export const playwright = defineConfig({
  fullyParallel: true,
  workers: process.env.CI ? 1 : undefined,
  use: {
    baseURL: 'http://localhost:3000',
    trace: 'on-first-retry',
  },
  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
    },
  ],
  webServer: {
    command: 'bun rsbuild build && bun rsbuild preview',
    url: 'http://localhost:3000',
    reuseExistingServer: !process.env.CI,
  },
})

The above is an example of a TypeScript configuration.ts file that will automatically configure Playwright in your project.

During the installation of the project, these files are read, and all the necessary configuration files are created. To prevent these files from bothering you or being committed, they are automatically ignored in the .gitignore, which can also be generated and doesn't have to be committed to the source anymore.

Configuration files are ignored

As seen above, most configuration files are grayed out and not part of the source code. This means that, at least, the version committed to GitHub will only have relevant files present. Previously, developers worked around this issue by placing all source code files in a /src folder. However, this is no longer necessary as the source code starts directly in the root of any project.

Open Issues

By nature of the approach, this type of plugin is very open-ended. Any supported configuration has to be manually added to the plugin initially, as there is no standard for all plugins. Additionally, the usage of shared configurations needs to be thoroughly tested and improved. Similarly, extending multiple configurations is planned. Furthermore, I'm trying to find a good way to set configurations that will automatically be added to any existing configurations present in the project.

Templates and New Technology Stack

The new technologies presented in the previous post, namely Bun, Biome, and Rsbuild, continue to age well, and I keep being pleasantly surprised. For that reason, zero-configuration, especially its templates, are all in on Bun as the runtime, Biome as the linter and formatter, and Rsbuild as the bundler.

bun create now zero-configuration # Default template (React app built and served with Bun)
bun create now zero-configuration . web # Fully featured React application using Rsbuild
bun create now zero-configuration . plugin # TypeScript plugin
bun create now zero-configuration . plugin-react # Plugin with JSX

Using the create-now package, you can check out some templates and get started right away. I've also updated create-now to use zero-configuration and Bun as the runtime.

Especially for plugins, I've recently tested a completely bundler-less approach that's also part of the above plugin and plugin-react. The idea there is to publish TypeScript and JSX directly to npm. This works great for modern Web Development, as bundlers like Rsbuild or Vite can easily and quickly bundle the code. For users or yourself as the plugin developer, it's much easier to quickly debug or make changes to the plugin. Examples include masua,overflow-scroll-fade, optica, and epic-cli. It's worth noting that I'm pioneering this approach, and it may not become the default way to publish plugins anytime soon. Idea-wise, it follows the same approach as zero-configuration, in that generated code should be avoided whenever possible, and the source should be as close as possible to human-readable code, until the destination is the client machine (the browser), where the code is finally executed.

@matthiasgigers avatar image

Matthias Giger

@matthiasgiger · Apr 13

I have recently started publishing the source code of some more modern web development plugins, utilizing TypeScript and JSX. I haven't yet seen anybody else do this, but it offers some interesting advantages. Here are some of the lessons I've learned:

• It works out-of-the-box with Vite and Rsbuild, but not with Next.js (might be configurable).
• Plugin development itself becomes simpler as no build tool and almost no dependencies are required.
• Performance is not an issue for small libraries, but generated types without much inference might be noticeably faster for larger libraries.
• "skipLibCheck": true will not skip the checking of TypeScript source code. This setting only applies to declaration files within node_modules.
• Publishing a tsconfig.json will not have an effect, but it's crucial that the user configuration aligns with your code. This is relatively easy when you enable strict mode and avoid relying on recent features (think of ES2020 as a default). For necessary additions, it might be helpful to link your tsconfig.json in the installation instructions, in case the user encounters any non-obvious issues. Additionally, along with the installation instructions, it makes sense to list the required type dependencies. Instead of adding types as dependencies, it's better to rely on inference, allowing the user to choose their specific type set.

This post was revised with ChatGPT a Large Language Model.