diff --git a/README.md b/README.md index 815ef4b..859a65a 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,8 @@ Many web development projects often contain numerous configuration files in the - No configuration files in your source code. - Support for **gitignore**, **TypeScript**, **ESLint**, **Prettier**, **Biome**, **VS Code** and **Playwright**. +- Quickly configure bundlers like **Vite** and **Rsbuild**. +- Generate boilerplate before publishing: **LICENSE.md**. - JSON based configuration in `package.json`. - Optional typed programmatic interface in `configuration.ts`. - Recommended configurations to easily extend. diff --git a/configuration/index.ts b/configuration/index.ts new file mode 100644 index 0000000..498a6c6 --- /dev/null +++ b/configuration/index.ts @@ -0,0 +1,53 @@ +import type { Configuration } from '../types' +import * as biome from './biome' +import * as eslint from './eslint' +import * as ignore from './gitignore' +import * as license from './license' +import * as playwright from './playwright' +import * as prettier from './prettier' +import * as rsbuild from './rsbuild' +import * as typescript from './typescript' +import * as vite from './vite' +import * as vscode from './vscode' + +export { ignore } + +export const configurations: Configuration[] = [ + { + name: 'typescript', + alias: 'tsconfig', + configuration: typescript, + }, + { + name: 'biome', + configuration: biome, + }, + { + name: 'eslint', + configuration: eslint, + }, + { + name: 'prettier', + configuration: prettier, + }, + { + name: 'vscode', + configuration: vscode, + }, + { + name: 'playwright', + configuration: playwright, + }, + { + name: 'vite', + configuration: vite, + }, + { + name: 'rsbuild', + configuration: rsbuild, + }, + { + name: 'license', + configuration: license, + }, +] diff --git a/configuration/license.ts b/configuration/license.ts new file mode 100644 index 0000000..cd9c148 --- /dev/null +++ b/configuration/license.ts @@ -0,0 +1,45 @@ +import type { PackageJson, Template } from '../types' + +const getNameFromPackageJson = (packageJson: PackageJson) => { + if (typeof packageJson.author === 'string') { + return packageJson.author + } + + if (typeof packageJson.author === 'object' && typeof packageJson.author.name === 'string') { + return packageJson.author.name + } + + return packageJson.name +} + +const mitLicense = (packageJson: PackageJson) => `MIT License + +Copyright (c) ${new Date().getFullYear()} ${getNameFromPackageJson(packageJson)} + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE.` + +export const templates: Template<(packageJson: PackageJson) => string> = { + mit: mitLicense, + // biome-ignore lint/style/useNamingConvention: Alias for upper case typed license. + MIT: mitLicense, +} + +export function createFile(content: string) { + return { name: 'LICENSE.md', contents: content } +} diff --git a/configuration/rsbuild.ts b/configuration/rsbuild.ts new file mode 100644 index 0000000..f5581f8 --- /dev/null +++ b/configuration/rsbuild.ts @@ -0,0 +1,5 @@ +export const extension = (path: string) => ({ extends: path }) + +export function createFile(configuration: object) { + return { name: 'rsbuild.config.ts', contents: `export default ${JSON.stringify(configuration, null, 2)}` } +} diff --git a/configuration/vite.ts b/configuration/vite.ts new file mode 100644 index 0000000..974d647 --- /dev/null +++ b/configuration/vite.ts @@ -0,0 +1,5 @@ +export const extension = (path: string) => ({ extends: path }) + +export function createFile(configuration: object) { + return { name: 'vite.config.js', contents: `export default ${JSON.stringify(configuration, null, 2)}` } +} diff --git a/helper.ts b/helper.ts new file mode 100644 index 0000000..87c50be --- /dev/null +++ b/helper.ts @@ -0,0 +1,23 @@ +import { create } from 'logua' +import { z } from 'zod' +import { configurations } from './configuration' + +export const log = create('zero-configuration', 'blue') + +const keys = Object.fromEntries(configurations.map((current) => [current.name, z.union([z.string(), z.object({}), z.boolean()])])) + +for (const configuration of configurations) { + if (configuration.alias) { + keys[configuration.alias] = z.union([z.string(), z.object({}), z.boolean()]) + } +} + +const schema = z.object(keys).partial().strip() + +export const validate = (configuration: unknown) => { + try { + return schema.parse(configuration) + } catch (error) { + log(`Invalid configuration provided: ${error}`, 'error') + } +} diff --git a/index.ts b/index.ts index 9438210..5dfddd1 100644 --- a/index.ts +++ b/index.ts @@ -3,53 +3,13 @@ import { existsSync } from 'node:fs' import { join } from 'node:path' import { it } from 'avait' import Bun from 'bun' -import { create } from 'logua' -import * as biome from './configuration/biome' -import * as eslint from './configuration/eslint' -import * as ignore from './configuration/gitignore' -import * as playwright from './configuration/playwright' -import * as prettier from './configuration/prettier' -import * as typescript from './configuration/typescript' -import * as vscode from './configuration/vscode' +import { configurations, ignore } from './configuration' +import { log, validate } from './helper' import { parse } from './parse' -import type { Configuration } from './types' - -const log = create('zero-configuration', 'blue') - -// TODO validate inputs with zod. - -const configurations: Configuration[] = [ - { - name: 'typescript', - alias: 'tsconfig', - configuration: typescript, - }, - { - name: 'biome', - configuration: biome, - }, - { - name: 'eslint', - configuration: eslint, - }, - { - name: 'prettier', - configuration: prettier, - }, - { - name: 'vscode', - configuration: vscode, - }, - { - name: 'playwright', - configuration: playwright, - }, -] const ignores: string[] = [] const packageJson = await Bun.file('./package.json').json() -// @ts-ignore const { value: moduleContents } = await it(import(join(process.cwd(), './configuration.ts'))) if (!(moduleContents || Object.hasOwn(packageJson, 'configuration'))) { @@ -58,10 +18,12 @@ if (!(moduleContents || Object.hasOwn(packageJson, 'configuration'))) { const userConfiguration = packageJson.configuration ?? moduleContents +validate(userConfiguration) + for (const { name, alias, configuration } of configurations) { const value = userConfiguration[name] ?? (alias && userConfiguration[alias]) if (!value) continue - const file = await parse(value, configuration) + const file = await parse(value, configuration, packageJson) if (!file) continue await Bun.write(file.name, file.contents) ignores.push(file.name) diff --git a/package.json b/package.json index cc8fcf8..66fd205 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,8 @@ }, "dependencies": { "avait": "^1.0.0", - "logua": "^3.0.3" + "logua": "^3.0.3", + "zod": "^3.22.5" }, "devDependencies": { "@biomejs/biome": "^1.7.0", diff --git a/parse.ts b/parse.ts index aa0ae20..7f28ce3 100644 --- a/parse.ts +++ b/parse.ts @@ -1,7 +1,7 @@ import { existsSync } from 'node:fs' import { join } from 'node:path' import { it } from 'avait' -import type { Configuration } from './types' +import type { Configuration, PackageJson } from './types' const isExtension = async (value: string) => { // NOTE dynamic import in tests will resolve relative to project node_modules and not fixture. @@ -13,10 +13,11 @@ const isExtension = async (value: string) => { return false } -export async function parse(value: string | object | boolean, configuration: Configuration['configuration']) { +export async function parse(value: string | object | boolean, configuration: Configuration['configuration'], packageJson: PackageJson) { // Template. - if (typeof value === 'string' && Object.hasOwn(configuration.templates, value)) { - const configurationTemplate = configuration.templates[value as keyof typeof configuration.templates] + if (typeof value === 'string' && configuration.templates && Object.hasOwn(configuration.templates, value)) { + const template = configuration.templates[value as keyof typeof configuration.templates] + const configurationTemplate = typeof template === 'function' ? (template as (value: PackageJson) => string)(packageJson) : template return configuration.createFile(configurationTemplate) } diff --git a/test/basic.test.ts b/test/basic.test.ts index ca2f8fa..f2c2aae 100644 --- a/test/basic.test.ts +++ b/test/basic.test.ts @@ -13,6 +13,7 @@ test('Adds configuration files for basic package setup.', () => { expect(existsSync(join(fixturePath, 'prettier.config.js'))).toBe(true) expect(existsSync(join(fixturePath, 'biome.json'))).toBe(true) + expect(existsSync(join(fixturePath, 'LICENSE.md'))).toBe(true) }) test('Adds configuration files for basic file setup.', () => { diff --git a/test/fixture/package/package.json b/test/fixture/package/package.json index 409dc86..99de4a0 100644 --- a/test/fixture/package/package.json +++ b/test/fixture/package/package.json @@ -1,10 +1,14 @@ { "name": "package", + "author": { + "name": "Matthias Giger" + }, "configuration": { "eslint": true, "prettier": "recommended", "biome": "recommended", "playwright": true, + "license": "MIT", "tsconfig": { "compilerOptions": { "target": "ES6" diff --git a/test/unit.test.ts b/test/unit.test.ts new file mode 100644 index 0000000..b1bfaf8 --- /dev/null +++ b/test/unit.test.ts @@ -0,0 +1,21 @@ +import { expect, test } from 'bun:test' +import { validate } from '../helper' + +test('Configuration is validated properly.', () => { + const configuration = { + biome: 'recommended', + prettier: true, + typescript: 'test/tsconfig.json', + tsconfig: 'my-module/tsconfig.json', + removed: 'not-available', + } + + const validated = validate(configuration) + + expect(validated).toBeDefined() + expect(typeof validated).toBe('object') + // Strips missing entries. + expect(validated && Object.hasOwn(validated, 'removed')).toBe(false) + // Keeps aliases. + expect(typeof validated?.tsconfig).toBe('string') +}) diff --git a/types.ts b/types.ts index 72517ec..12f4bac 100644 --- a/types.ts +++ b/types.ts @@ -1,10 +1,12 @@ export type Template = { [key: string]: T } +export type PackageJson = { name: string; author?: string | { name: string } } + export type Configuration = { name: string alias?: string configuration: { - templates: Template + templates?: Template | ((packageJson: PackageJson) => string) // biome-ignore lint/suspicious/noExplicitAny: Will be specified in file explicitly. createFile: (value?: any) => { name: string; contents: string } extension?: (path: string) => object