Skip to content

feat: validate configuration with arktype #806

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/react-native-builder-bob/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
82 changes: 29 additions & 53 deletions packages/react-native-builder-bob/src/build.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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'
);

Expand All @@ -94,68 +70,69 @@ 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,
})
)
);
}
}

async function buildTarget({
async function buildTarget<T extends Target>({
root,
target,
source,
output,
exclude,
options,
config,
variants,
}: {
root: string;
target: Exclude<Options['targets'], undefined>[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,
});
break;
case 'typescript':
{
const esm =
options.targets?.some((t) => {
config.targets?.some((t) => {
if (Array.isArray(t)) {
const [name, options] = t;

Expand All @@ -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,
Expand All @@ -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)}.`);
}
}
2 changes: 1 addition & 1 deletion packages/react-native-builder-bob/src/index.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down
87 changes: 87 additions & 0 deletions packages/react-native-builder-bob/src/schema.ts
Original file line number Diff line number Diff line change
@@ -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 Target> = 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;
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import del from 'del';
import { runRNCCli } from '../../utils/runRNCCli';
import { removeCodegenAppLevelCode } from './patches/removeCodegenAppLevelCode';

type Options = Input;
type Options = Omit<Input, 'output'>;

export default async function build({ root, report }: Options) {
const packageJsonPath = path.resolve(root, 'package.json');
Expand Down
15 changes: 1 addition & 14 deletions packages/react-native-builder-bob/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export type Log = (message: string) => void;

export type Report = {
info: Log;
warn: Log;
Expand All @@ -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;
Expand Down
4 changes: 3 additions & 1 deletion packages/react-native-builder-bob/src/utils/loadConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
3 changes: 2 additions & 1 deletion packages/react-native-builder-bob/src/utils/workerize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<T extends Target> = {
target: T;
Expand Down
27 changes: 27 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down
Loading