diff --git a/configuration/next.ts b/configuration/next.ts index 798d8d1..834d0e7 100644 --- a/configuration/next.ts +++ b/configuration/next.ts @@ -13,7 +13,7 @@ export function createFile(configuration: object | string) { export default next` - if (typeof configuration === 'object') { + if (typeof configuration === 'object' && state.language === 'json') { contents = `export default ${JSON.stringify(configuration, null, 2)}` } diff --git a/configuration/playwright.ts b/configuration/playwright.ts index c57f626..e24c309 100644 --- a/configuration/playwright.ts +++ b/configuration/playwright.ts @@ -1,13 +1,15 @@ -import type { Template } from '../types' +import { fileExtension, state } from '../state' -export const templates: Template = {} // Has no templates, highly customizable. +export const extension = (path: string) => ({ extends: path }) -export function createFile() { - return { - name: 'playwright.config.ts', - contents: `import { defineConfig } from '@playwright/test' -import { playwright } from './configuration.ts' +export function createFile(configuration: object | string) { + let contents = `import { playwright } from './configuration.${fileExtension()}' -export default defineConfig(playwright)`, +export default playwright` + + if (typeof configuration === 'object' && state.language === 'json') { + contents = `export default ${JSON.stringify(configuration, null, 2)}` } + + return { name: `playwright.config.${fileExtension()}`, contents } } diff --git a/configuration/rsbuild.ts b/configuration/rsbuild.ts index 0c52533..d885fba 100644 --- a/configuration/rsbuild.ts +++ b/configuration/rsbuild.ts @@ -1,4 +1,4 @@ -import { fileExtension } from '../state' +import { fileExtension, state } from '../state' export const extension = (path: string) => ({ extends: path }) @@ -7,7 +7,7 @@ export function createFile(configuration: object | string) { export default rsbuild` - if (typeof configuration === 'object') { + if (typeof configuration === 'object' && state.language === 'json') { contents = `export default ${JSON.stringify(configuration, null, 2)}` } diff --git a/configuration/typescript.ts b/configuration/typescript.ts index 921942c..c8718a5 100644 --- a/configuration/typescript.ts +++ b/configuration/typescript.ts @@ -4,6 +4,15 @@ export const templates = { strict: true, }, }, + plugin: { + compilerOptions: { + strict: true, + skipLibCheck: true, + target: 'ES2020', + lib: ['DOM', 'ES2020'], + module: 'Preserve', + }, + }, } export const extension = (path: string) => ({ extends: path }) diff --git a/configuration/vite.ts b/configuration/vite.ts index 6979180..29a5522 100644 --- a/configuration/vite.ts +++ b/configuration/vite.ts @@ -1,4 +1,4 @@ -import { fileExtension } from '../state' +import { fileExtension, state } from '../state' export const extension = (path: string) => ({ extends: path }) @@ -7,7 +7,7 @@ export function createFile(configuration: object | string) { export default vite` - if (typeof configuration === 'object') { + if (typeof configuration === 'object' && state.language === 'json') { contents = `export default ${JSON.stringify(configuration, null, 2)}` } diff --git a/configuration/vscode.ts b/configuration/vscode.ts index 74b1823..2366b46 100644 --- a/configuration/vscode.ts +++ b/configuration/vscode.ts @@ -1,7 +1,7 @@ import type { Template } from '../types' export const templates: Template = { - recommended: { + biome: { 'editor.defaultFormatter': 'biomejs.biome', 'editor.codeActionsOnSave': { 'quickfix.biome': 'explicit', @@ -9,6 +9,13 @@ export const templates: Template = { }, 'editor.formatOnSave': true, }, + 'prettier-eslint': { + 'editor.defaultFormatter': 'esbenp.prettier-vscode', + 'editor.codeActionsOnSave': { + 'source.fixAll.eslint': true, + }, + 'editor.formatOnSave': true, + }, } export function createFile(configuration: object) { diff --git a/helper.ts b/helper.ts index cd0d9f4..f043580 100644 --- a/helper.ts +++ b/helper.ts @@ -1,18 +1,14 @@ -import { existsSync } from 'node:fs' -import { join } from 'node:path' +import { existsSync, lstatSync } from 'node:fs' import { it } from 'avait' -import Bun from 'bun' +import Bun, { Glob } from 'bun' import { create } from 'logua' import { parse } from 'parse-gitignore' import { z } from 'zod' import { configurations, ignore } from './configuration' -import { state } from './state' +import { root, state } from './state' export const log = create('zero-configuration', 'blue') -export const root = (file: string) => - process.cwd().includes('node_modules') ? join(process.cwd(), '../..', file) : join(process.cwd(), file) - const keys = Object.fromEntries(configurations.map((current) => [current.name, z.union([z.string(), z.object({}), z.boolean()])])) for (const configuration of configurations) { @@ -84,11 +80,31 @@ export async function writeGitIgnore(ignores: string[]) { const file = ignore.createFile(userIgnores as string[]) - if (existsSync(file.name)) { + if (existsSync(root(file.name))) { await addAdditionalGitignoreEntries(file) - } else { + } else if (state.root) { await Bun.write(root(file.name), file.contents) } return Object.hasOwn(state.options, 'ignore') || Object.hasOwn(state.options, 'gitignore') } + +export async function getWorkspaces() { + const packageJson = await Bun.file(root('./package.json')).json() + const workspaces: { path: string; root: boolean }[] = [] + + if (Array.isArray(packageJson.workspaces)) { + for (const workspaceGlob of packageJson.workspaces) { + const glob = new Glob(workspaceGlob) + for await (const file of glob.scan({ cwd: root('/'), dot: false, onlyFiles: false })) { + if (lstatSync(file).isDirectory()) { + workspaces.push({ path: file, root: false }) + } + } + } + } + + workspaces.push({ path: '/', root: true }) // Always run in root, root should run last. + + return workspaces +} diff --git a/index.ts b/index.ts index fc46822..d1c641a 100644 --- a/index.ts +++ b/index.ts @@ -1,25 +1,32 @@ #!/usr/bin/env bun import Bun from 'bun' import { configurations } from './configuration' -import { findConfiguration, log, root, writeGitIgnore } from './helper' +import { findConfiguration, getWorkspaces, log, writeGitIgnore } from './helper' import { parse } from './parse' -import { state } from './state' +import { reset, root, state } from './state' -const ignores: string[] = [] +async function configureProject() { + const ignores: string[] = [] -await findConfiguration() + await findConfiguration() -for (const { name, alias, configuration } of configurations) { - const value = state.options[name] ?? (alias && state.options[alias]) - if (!value) continue - const file = await parse(value, configuration) - if (!file) continue - await Bun.write(root(file.name), file.contents) - ignores.push(file.name) -} + for (const { name, alias, configuration } of configurations) { + const value = state.options[name] ?? (alias && state.options[alias]) + if (!value) continue + const file = await parse(value, configuration) + if (!file) continue + await Bun.write(root(file.name), file.contents) + ignores.push(file.name) + } + + const gitUserConfigured = await writeGitIgnore(ignores) -const gitUserConfigured = await writeGitIgnore(ignores) + if (!gitUserConfigured && ignores.length === 0) { + log('No configuration to add', 'warning') + } +} -if (!gitUserConfigured && ignores.length === 0) { - log('No configuration to add', 'warning') +for (const workspace of await getWorkspaces()) { + reset(workspace) + await configureProject() } diff --git a/package.json b/package.json index 8ed6351..4d838dc 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,15 @@ "bun.lockb", "test/fixture/*/*", "!test/fixture/*/package.json", + "!test/fixture/workspaces/package.json", + "!test/fixture/workspaces/plugin", + "test/fixture/workspaces/plugin/*", + "!test/fixture/workspaces/plugin/package.json", + "!test/fixture/workspaces/demo", + "test/fixture/workspaces/demo/*", + "!test/fixture/workspaces/demo/react", + "test/fixture/workspaces/demo/react/*", + "!test/fixture/workspaces/demo/react/package.json", "!test/fixture/*/configuration.ts", "!test/fixture/*/configuration.js" ], @@ -70,18 +79,9 @@ ] } }, - "vscode": "recommended", + "vscode": "biome", "typescript": { - "compilerOptions": { - "strict": true, - "skipLibCheck": true, - "target": "ES2020", - "lib": [ - "DOM", - "ES2020" - ], - "module": "Preserve" - }, + "extends": "plugin", "files": [ "index.ts" ] diff --git a/parse.ts b/parse.ts index 0706d7e..819d665 100644 --- a/parse.ts +++ b/parse.ts @@ -2,6 +2,7 @@ import { existsSync } from 'node:fs' import { join } from 'node:path' import { it } from 'avait' import { merge } from 'ts-deepmerge' +import { state } from './state' import type { Configuration, Options } from './types' const isExtension = async (value: string) => { @@ -56,6 +57,10 @@ export async function parse(value: Options, configuration: Configuration['config return configuration.createFile() } + if (typeof value === 'object' && state.language !== 'json') { + return configuration.createFile(value) + } + // biome-ignore lint/style/noParameterAssign: Easier in this case. value = extendTemplate(value, configuration) diff --git a/state.ts b/state.ts index 705a380..babf3eb 100644 --- a/state.ts +++ b/state.ts @@ -1,9 +1,23 @@ +import { join } from 'node:path' import type { State } from './types' export const state: State = { options: {}, language: 'json', packageJson: { name: 'missing-package-name' }, + directory: '/', + root: true, } export const fileExtension = () => (state.language === 'javascript' ? 'js' : 'ts') + +export const root = (file: string) => + process.cwd().includes('node_modules') ? join(process.cwd(), '../..', state.directory, file) : join(process.cwd(), state.directory, file) + +export const reset = ({ path, root }: { path: string; root: boolean }) => { + state.options = {} + state.language = 'json' + state.packageJson = { name: 'missing-package-name' } + state.directory = path + state.root = root +} diff --git a/test/basic.test.ts b/test/basic.test.ts index 506c638..3437722 100644 --- a/test/basic.test.ts +++ b/test/basic.test.ts @@ -82,3 +82,16 @@ test('Creates configuration files for various build-tool configurations.', () => expect(existsSync(join(fixturePath, 'rsbuild.config.ts'))).toBe(true) expect(existsSync(join(fixturePath, 'vite.config.ts'))).toBe(true) }) + +test('Creates configuration files in all workspaces including the root.', () => { + const fixturePath = './test/fixture/workspaces' + + execSync('bun ./../../../index.ts', { + cwd: fixturePath, + stdio: 'inherit', + }) + + expect(existsSync(join(fixturePath, 'tsconfig.json'))).toBe(true) + expect(existsSync(join(fixturePath, 'demo/react/tsconfig.json'))).toBe(true) + expect(existsSync(join(fixturePath, 'plugin/tsconfig.json'))).toBe(true) +}) diff --git a/test/fixture/workspaces/demo/react/package.json b/test/fixture/workspaces/demo/react/package.json new file mode 100644 index 0000000..ab34e1d --- /dev/null +++ b/test/fixture/workspaces/demo/react/package.json @@ -0,0 +1,7 @@ +{ + "name": "demo-react", + "configuration": { + "typescript": "plugin", + "vite": true + } +} \ No newline at end of file diff --git a/test/fixture/workspaces/package.json b/test/fixture/workspaces/package.json new file mode 100644 index 0000000..7d86210 --- /dev/null +++ b/test/fixture/workspaces/package.json @@ -0,0 +1,10 @@ +{ + "name": "workspaces", + "workspaces": [ + "demo/*", + "plugin" + ], + "configuration": { + "typescript": "plugin" + } +} \ No newline at end of file diff --git a/test/fixture/workspaces/plugin/package.json b/test/fixture/workspaces/plugin/package.json new file mode 100644 index 0000000..a961ea1 --- /dev/null +++ b/test/fixture/workspaces/plugin/package.json @@ -0,0 +1,6 @@ +{ + "name": "plugin", + "configuration": { + "typescript": "plugin" + } +} \ No newline at end of file diff --git a/types.ts b/types.ts index 465938d..d1c809d 100644 --- a/types.ts +++ b/types.ts @@ -23,4 +23,6 @@ export interface State { // configuration.js: JavaScript, configuration.ts: TypeScript language: 'json' | 'javascript' | 'typescript' packageJson: PackageJson + directory: string + root: boolean }