diff --git a/packages/react-native-builder-bob/package.json b/packages/react-native-builder-bob/package.json index c37158a9..bbf06ee9 100644 --- a/packages/react-native-builder-bob/package.json +++ b/packages/react-native-builder-bob/package.json @@ -51,6 +51,7 @@ "@babel/preset-flow": "^7.24.7", "@babel/preset-react": "^7.24.7", "@babel/preset-typescript": "^7.24.7", + "arktype": "^2.1.15", "babel-plugin-module-resolver": "^5.0.2", "browserslist": "^4.20.4", "cross-spawn": "^7.0.3", diff --git a/packages/react-native-builder-bob/src/build.ts b/packages/react-native-builder-bob/src/build.ts index 4e1fd440..914b4305 100644 --- a/packages/react-native-builder-bob/src/build.ts +++ b/packages/react-native-builder-bob/src/build.ts @@ -1,8 +1,9 @@ +import { type } from 'arktype'; import fs from 'fs-extra'; import kleur from 'kleur'; import path from 'path'; import yargs from 'yargs'; -import { type Options, type Target } from './types'; +import { config, type Config, type Target, type TargetOptions } from './schema'; import { loadConfig } from './utils/loadConfig'; import * as logger from './utils/logger'; import { run } from './utils/workerize'; @@ -39,46 +40,21 @@ export async function build(argv: Argv) { ); } - const options: Options = result!.config; + const parsed = config(result.config); - if (!options.targets?.length) { + if (parsed instanceof type.errors) { throw new Error( - `No 'targets' found in the configuration in '${path.relative( - root, - result!.filepath - )}'.` - ); - } - - const source = options.source; - - if (!source) { - throw new Error( - `No 'source' option found in the configuration in '${path.relative( - root, - result!.filepath - )}'.` + `Invalid configuration in ${result.filepath}: ${parsed.summary}` ); } - const output = options.output; + const { source, output, targets, exclude } = parsed; - if (!output) { - throw new Error( - `No 'output' option found in the configuration in '${path.relative( - root, - result!.filepath - )}'.` - ); - } - - const exclude = options.exclude ?? '**/{__tests__,__fixtures__,__mocks__}/**'; - - const commonjs = options.targets?.some((t) => + const commonjs = targets.some((t) => Array.isArray(t) ? t[0] === 'commonjs' : t === 'commonjs' ); - const module = options.targets?.some((t) => + const module = targets.some((t) => Array.isArray(t) ? t[0] === 'module' : t === 'module' ); @@ -94,19 +70,19 @@ export async function build(argv: Argv) { source, output, exclude, - options, + config: parsed, variants, }); } else { await Promise.all( - options.targets?.map((target) => + targets.map((target) => buildTarget({ root, - target, + target: Array.isArray(target) ? target[0] : target, source, output, exclude, - options, + config: parsed, variants, }) ) @@ -114,40 +90,41 @@ export async function build(argv: Argv) { } } -async function buildTarget({ +async function buildTarget({ root, target, source, output, exclude, - options, + config, variants, }: { root: string; - target: Exclude[number]; + target: T; source: string; output: string; exclude: string; - options: Options; + config: Config; variants: { commonjs?: boolean; module?: boolean; }; }) { - const targetName = Array.isArray(target) ? target[0] : target; - const targetOptions = Array.isArray(target) ? target[1] : undefined; + const options = config.targets + .map((t) => (Array.isArray(t) ? t : ([t, undefined] as const))) + .find((t) => t[0] === target)?.[1]; - const report = logger.grouped(targetName); + const report = logger.grouped(target); - switch (targetName) { + switch (target) { case 'commonjs': case 'module': - await run(targetName, { + await run(target, { root, source: path.resolve(root, source), - output: path.resolve(root, output, targetName), + output: path.resolve(root, output, target), exclude, - options: targetOptions, + options: options as TargetOptions<'commonjs' | 'module'>, variants, report, }); @@ -155,7 +132,7 @@ async function buildTarget({ case 'typescript': { const esm = - options.targets?.some((t) => { + config.targets?.some((t) => { if (Array.isArray(t)) { const [name, options] = t; @@ -171,7 +148,7 @@ async function buildTarget({ root, source: path.resolve(root, source), output: path.resolve(root, output, 'typescript'), - options: targetOptions, + options: options as TargetOptions<'typescript'>, esm, variants, report, @@ -182,19 +159,18 @@ async function buildTarget({ await run('codegen', { root, source: path.resolve(root, source), - output: path.resolve(root, output, 'typescript'), report, }); break; case 'custom': await run('custom', { - options: targetOptions, + root, source: path.resolve(root, source), + options: options as TargetOptions<'custom'>, report, - root, }); break; default: - throw new Error(`Invalid target ${kleur.blue(targetName)}.`); + throw new Error(`Invalid target ${kleur.blue(target)}.`); } } diff --git a/packages/react-native-builder-bob/src/index.ts b/packages/react-native-builder-bob/src/index.ts index 62e92d4f..7271cd16 100644 --- a/packages/react-native-builder-bob/src/index.ts +++ b/packages/react-native-builder-bob/src/index.ts @@ -1,7 +1,7 @@ import yargs from 'yargs'; import { build } from './build'; import { init } from './init'; -import type { Target } from './types'; +import type { Target } from './schema'; type ArgName = 'target'; diff --git a/packages/react-native-builder-bob/src/schema.ts b/packages/react-native-builder-bob/src/schema.ts new file mode 100644 index 00000000..858755d8 --- /dev/null +++ b/packages/react-native-builder-bob/src/schema.ts @@ -0,0 +1,87 @@ +import { type } from 'arktype'; + +const module = { + name: '"module"', + options: type({ + esm: type('boolean').default(false), + babelrc: type('boolean').default(false), + configFile: type('boolean').default(false), + sourceMaps: type('boolean').default(true), + copyFlow: type('boolean').default(false), + jsxRuntime: type('"automatic" | "classic"').default('automatic'), + }), +} as const; + +const commonjs = { + name: '"commonjs"', + options: module.options, +} as const; + +const typescript = { + name: '"typescript"', + options: type({ + project: 'string?', + tsc: 'string?', + }), +} as const; + +const codegen = { + name: '"codegen"', +} as const; + +const custom = { + name: '"custom"', + options: type({ + script: 'string', + clean: 'string?', + }), +} as const; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const target = type.or( + commonjs.name, + module.name, + typescript.name, + codegen.name, + custom.name +); + +export const config = type({ + source: 'string', + output: 'string', + targets: type + .or( + type.or(module.name, [module.name], [module.name, module.options]), + type.or( + commonjs.name, + [commonjs.name], + [commonjs.name, commonjs.options] + ), + type.or( + typescript.name, + [typescript.name], + [typescript.name, typescript.options] + ), + type.or(codegen.name, [codegen.name]), + [custom.name, custom.options] + ) + .array() + .moreThanLength(0), + exclude: type.string.default('**/{__tests__,__fixtures__,__mocks__}/**'), +}).onDeepUndeclaredKey('reject'); + +export type Config = typeof config.infer; + +export type Target = typeof target.infer; + +export type TargetOptions = T extends typeof commonjs.name + ? typeof commonjs.options.infer + : T extends typeof module.name + ? typeof module.options.infer + : T extends typeof typescript.name + ? typeof typescript.options.infer + : T extends typeof custom.name + ? typeof custom.options.infer + : T extends typeof codegen.name + ? undefined + : never; diff --git a/packages/react-native-builder-bob/src/targets/codegen/index.ts b/packages/react-native-builder-bob/src/targets/codegen/index.ts index 596c0b2c..33e6c6e2 100644 --- a/packages/react-native-builder-bob/src/targets/codegen/index.ts +++ b/packages/react-native-builder-bob/src/targets/codegen/index.ts @@ -7,7 +7,7 @@ import del from 'del'; import { runRNCCli } from '../../utils/runRNCCli'; import { removeCodegenAppLevelCode } from './patches/removeCodegenAppLevelCode'; -type Options = Input; +type Options = Omit; export default async function build({ root, report }: Options) { const packageJsonPath = path.resolve(root, 'package.json'); diff --git a/packages/react-native-builder-bob/src/types.ts b/packages/react-native-builder-bob/src/types.ts index 80ac0285..90f034b3 100644 --- a/packages/react-native-builder-bob/src/types.ts +++ b/packages/react-native-builder-bob/src/types.ts @@ -1,4 +1,5 @@ export type Log = (message: string) => void; + export type Report = { info: Log; warn: Log; @@ -13,20 +14,6 @@ export type Input = { report: Report; }; -export type Target = - | 'commonjs' - | 'module' - | 'typescript' - | 'codegen' - | 'custom'; - -export type Options = { - source?: string; - output?: string; - targets?: (Target | [target: Target, options: object])[]; - exclude?: string; -}; - export type Variants = { commonjs?: boolean; module?: boolean; diff --git a/packages/react-native-builder-bob/src/utils/loadConfig.ts b/packages/react-native-builder-bob/src/utils/loadConfig.ts index 072b34fd..eda33348 100644 --- a/packages/react-native-builder-bob/src/utils/loadConfig.ts +++ b/packages/react-native-builder-bob/src/utils/loadConfig.ts @@ -8,7 +8,9 @@ const searchPlaces = [ 'package.json', ]; -export const loadConfig = (root: string) => { +export const loadConfig = ( + root: string +): { filepath: string; config: unknown } | undefined => { for (const filename of searchPlaces) { const result = requireConfig(root, filename); diff --git a/packages/react-native-builder-bob/src/utils/workerize.ts b/packages/react-native-builder-bob/src/utils/workerize.ts index f033018f..22531a1e 100644 --- a/packages/react-native-builder-bob/src/utils/workerize.ts +++ b/packages/react-native-builder-bob/src/utils/workerize.ts @@ -9,7 +9,8 @@ import commonjs from '../targets/commonjs'; import custom from '../targets/custom'; import module from '../targets/module'; import typescript from '../targets/typescript'; -import type { Report, Target } from '../types'; +import type { Report } from '../types'; +import type { Target } from '../schema'; type WorkerData = { target: T; diff --git a/yarn.lock b/yarn.lock index 9fefa3dc..54fc15b7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -22,6 +22,22 @@ __metadata: languageName: node linkType: hard +"@ark/schema@npm:0.45.5": + version: 0.45.5 + resolution: "@ark/schema@npm:0.45.5" + dependencies: + "@ark/util": 0.45.5 + checksum: d4cd8f3e67f785f1a499e966bc9d10d43597a2dee26ae3226c681d9dfa5e88c8229c989b30703b785fa009eeef7158c54e52ca4bdebe67940e9bb0295d03a185 + languageName: node + linkType: hard + +"@ark/util@npm:0.45.5": + version: 0.45.5 + resolution: "@ark/util@npm:0.45.5" + checksum: 2074b5c0055b3ac857e33c6ffd26463cdd2813aad46b280de8238edf2b7e8a7264e03e73c2e2890cfe2af589742fb982ebaee3f60605238a14b0eb0a0262eb14 + languageName: node + linkType: hard + "@babel/cli@npm:^7.24.8": version: 7.24.8 resolution: "@babel/cli@npm:7.24.8" @@ -4448,6 +4464,16 @@ __metadata: languageName: node linkType: hard +"arktype@npm:^2.1.15": + version: 2.1.15 + resolution: "arktype@npm:2.1.15" + dependencies: + "@ark/schema": 0.45.5 + "@ark/util": 0.45.5 + checksum: 2f407713f42a7976362c5394435c841b9913e32722c8e0cbf2313a5931a5e7dad64b1efc8fbcac79fd4e7a58be22ab68b6b9c07b7f19abb65862ed548429006a + languageName: node + linkType: hard + "array-differ@npm:^3.0.0": version: 3.0.0 resolution: "array-differ@npm:3.0.0" @@ -12723,6 +12749,7 @@ __metadata: "@types/prompts": ^2.0.14 "@types/which": ^2.0.1 "@types/yargs": ^17.0.10 + arktype: ^2.1.15 babel-plugin-module-resolver: ^5.0.2 browserslist: ^4.20.4 concurrently: ^7.2.2