Skip to content

Commit

Permalink
feat(configuration): support generating multiple files
Browse files Browse the repository at this point in the history
release-npm
  • Loading branch information
tobua committed Jul 1, 2024
1 parent 009d8ff commit 7cc5b7c
Show file tree
Hide file tree
Showing 9 changed files with 129 additions and 38 deletions.
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,21 @@ export const typescript = {
lib: ['ES2020', 'DOM']
}
}
// Generate multiple files.
export const tsconfig = [
{
extends: 'recommended'
},
{
extends: 'web',
folder: 'test/demo', // <= Specify folder!
compilerOptions: { skipLibCheck: false }
},
{
extends: 'react-native',
folder: 'app'
}
]
```

### All Available Options
Expand Down Expand Up @@ -91,3 +106,5 @@ export const license = 'MIT' | 'mit'
export const ignore = true | 'recommended' | 'bundle' | string[]
export const gitignore = // Alias for ignore
```

For any configuration it also passible to pass an array to generate multiple files. Using the `folder` property the destination of the configuration file can be set.
9 changes: 6 additions & 3 deletions helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,14 @@ import { log } from './log'
import { root, state } from './state'
import type { File } from './types'

const keys = Object.fromEntries(configurations.map((current) => [current.name, z.union([z.string(), z.object({}), z.boolean()])]))
const FileSchema = z.union([z.string(), z.object({}), z.boolean()])
const NestedFileSchema = z.union([FileSchema, z.array(FileSchema), z.undefined()])

const keys = Object.fromEntries(configurations.map((current) => [current.name, NestedFileSchema]))

for (const configuration of configurations) {
if (configuration.alias) {
keys[configuration.alias] = z.union([z.string(), z.object({}), z.boolean()])
keys[configuration.alias] = NestedFileSchema
}
}

Expand Down Expand Up @@ -91,7 +94,7 @@ export async function writeGitIgnore(ignores: string[]) {
// biome-ignore lint/style/noParameterAssign: Easier in this case.
ignores = ignores.map((ignore) => (ignore.includes('/') ? (ignore.split('/')[0] as string) : ignore))

let userIgnores = state.options.ignore ?? state.options.gitignore ?? ([] as string[])
let userIgnores = (state.options.ignore ?? state.options.gitignore ?? []) as string[]

if (typeof userIgnores === 'string' && Object.hasOwn(ignore.templates, userIgnores)) {
userIgnores = ignore.templates[userIgnores] as string[]
Expand Down
8 changes: 2 additions & 6 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,8 @@ async function configureProject() {
if (!value) continue
const files = await parse(value, configuration)
if (!files) continue
if (Array.isArray(files)) {
for (const file of files.filter((item) => item?.name)) {
await writeFile(file as File, ignores)
}
} else {
await writeFile(files as File, ignores)
for (const file of files.filter((item) => item?.name)) {
await writeFile(file as File, ignores)
}
}

Expand Down
14 changes: 9 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,23 @@
"check": "biome check --write .",
"postinstall": "bun index.ts",
"types": "tsc",
"test": "bun test ./test/*.test.ts"
"test": "bun test ./test/*.test.ts",
"clean": "git clean -fdx test/fixture"
},
"dependencies": {
"avait": "^1.0.0",
"avait": "^1.0.1",
"fast-glob": "^3.3.2",
"logua": "^3.0.3",
"parse-gitignore": "^2.0.0",
"ts-deepmerge": "^7.0.0",
"zod": "^3.23.8"
},
"devDependencies": {
"@biomejs/biome": "^1.8.1",
"@types/bun": "^1.1.4",
"@biomejs/biome": "^1.8.3",
"@types/bun": "^1.1.6",
"@types/parse-gitignore": "^1.0.2",
"eslint-config-airbnb": "^19.0.4",
"typescript": "^5.4.5"
"typescript": "^5.5.2"
},
"peerDependencies": {
"typescript": ">= 5"
Expand Down Expand Up @@ -90,6 +91,9 @@
"extends": "plugin",
"files": [
"index.ts"
],
"exclude": [
"test/fixture"
]
}
}
Expand Down
83 changes: 60 additions & 23 deletions parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { existsSync } from 'node:fs'
import { join } from 'node:path'
import { it } from 'avait'
import { merge } from 'ts-deepmerge'
import type { Configuration, Options } from './types'
import type { Configuration, File, Option, Options } from './types'

const isExtension = async (value: string) => {
// NOTE dynamic import in tests will resolve relative to project node_modules and not fixture.
Expand All @@ -14,50 +14,87 @@ const isExtension = async (value: string) => {
return false
}

function extendTemplate(value: Options, configuration: Configuration['configuration']) {
function extendTemplate(option: Option, configuration: Configuration['configuration']) {
if (
typeof value !== 'object' ||
!(typeof value.extends === 'string' && configuration.templates && Object.hasOwn(configuration.templates, value.extends))
typeof option !== 'object' ||
!(typeof option.extends === 'string' && configuration.templates && Object.hasOwn(configuration.templates, option.extends))
) {
return value
return option
}

let template = configuration.templates[value.extends]
let template = configuration.templates[option.extends]

if (typeof template === 'string') {
value.extends = template
option.extends = template
}
if (typeof template === 'function') {
template = template()
}
if (typeof template === 'object') {
// biome-ignore lint/performance/noDelete: We don't want the key to show up in the user configuration.
delete value.extends
return merge(template, value)
const optionWithoutPluginProperties = Object.fromEntries(Object.entries(option).filter(([key]) => !['extends', 'folder'].includes(key)))
return merge(template, optionWithoutPluginProperties)
}

return value
return option
}

export async function parse(value: Options, configuration: Configuration['configuration']) {
function addFolderToFile(file: File | undefined, folder?: string | false) {
if (file?.name && folder) {
file.name = join(folder, file.name)
}

return file
}

async function parseOption(option: Option, configuration: Configuration['configuration']) {
const folder = typeof option === 'object' && option.folder
let files: File | (File | undefined)[] | undefined = []
// Template.
if (typeof value === 'string' && configuration.templates && Object.hasOwn(configuration.templates, value)) {
const template = configuration.templates[value as keyof typeof configuration.templates]
if (typeof option === 'string' && configuration.templates && Object.hasOwn(configuration.templates, option)) {
const template = configuration.templates[option as keyof typeof configuration.templates]
const configurationTemplate = typeof template === 'function' ? template() : template
return configuration.createFile(configurationTemplate)
files = configuration.createFile(configurationTemplate)
} else if (typeof option === 'string' && (await isExtension(option)) && typeof configuration.extension === 'function') {
// File extension.
files = configuration.createFile(configuration.extension(option))
} else if (option === true) {
files = configuration.createFile(configuration.templates?.recommended)
} else {
// biome-ignore lint/style/noParameterAssign: Easier in this case.
option = extendTemplate(option, configuration)
files = configuration.createFile(option)
}

// File extension.
if (typeof value === 'string' && (await isExtension(value)) && typeof configuration.extension === 'function') {
return configuration.createFile(configuration.extension(value))
if (Array.isArray(files)) {
files = files.map((file) => {
return addFolderToFile(file, folder)
})
} else if (typeof files === 'object') {
files = addFolderToFile(files, folder)
}

if (value === true) {
return configuration.createFile(configuration.templates?.recommended)
return files
}

const unnestFileArray = (values: (File | (File | undefined)[] | undefined)[]): File[] => {
if (values.length === 1 && !values[0]) {
return []
}

// biome-ignore lint/style/noParameterAssign: Easier in this case.
value = extendTemplate(value, configuration)
return values.reduce((result, value) => {
if (value === undefined) return result
if (Array.isArray(value)) {
const nestedClean = value.filter((innerValue): innerValue is File => innerValue !== undefined)
return (result as File[]).concat(nestedClean)
}
return (result as File[]).concat(value)
}, []) as File[]
}

export async function parse(options: Options, configuration: Configuration['configuration']): Promise<File[]> {
if (!Array.isArray(options)) {
return unnestFileArray([await parseOption(options, configuration)])
}

return configuration.createFile(value)
return unnestFileArray(await Promise.all(options.map((option) => parseOption(option, configuration))))
}
17 changes: 17 additions & 0 deletions test/basic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,3 +204,20 @@ test('Also parses JavaScript configuration.', async () => {

expect(metroFile).toContain('unstable_enablePackageExports')
})

test('Can add multiple files.', async () => {
const fixturePath = './test/fixture/multiple'

execSync('bun ./../../../index.ts', {
cwd: fixturePath,
stdio: 'inherit',
})

expect(existsSync(join(fixturePath, 'tsconfig.json'))).toBe(true)
expect(await Bun.file(join(fixturePath, 'tsconfig.json')).text()).toContain('"strict": true')
expect(existsSync(join(fixturePath, 'test/tsconfig.json'))).toBe(true)
expect(await Bun.file(join(fixturePath, 'test/tsconfig.json')).text()).toContain('noUncheckedIndexedAccess')
expect(existsSync(join(fixturePath, 'demo/web/tsconfig.json'))).toBe(true)
// Make sure folder is removed.
expect(await Bun.file(join(fixturePath, 'demo/web/tsconfig.json')).text()).not.toContain('demo/web')
})
13 changes: 13 additions & 0 deletions test/fixture/multiple/configuration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export const typescript = [
{
extends: 'recommended'
},
{
folder: 'test',
extends: 'plugin'
},
{
folder: 'demo/web',
extends: 'web'
},
]
3 changes: 3 additions & 0 deletions test/fixture/multiple/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"name": "multiple"
}
3 changes: 2 additions & 1 deletion types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ export type Configuration = {
}
}

export type Options = string | { extends?: string } | true
export type Option = string | { extends?: string; folder?: string } | true
export type Options = Option | Option[]

export interface State {
options: { [Key in ConfigurationKeys]?: Options }
Expand Down

0 comments on commit 7cc5b7c

Please sign in to comment.