Skip to content

Commit a8fd434

Browse files
authored
feat: validate configuration with arktype (#806)
1 parent 7c12d62 commit a8fd434

File tree

9 files changed

+152
-71
lines changed

9 files changed

+152
-71
lines changed

packages/react-native-builder-bob/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
"@babel/preset-flow": "^7.24.7",
5252
"@babel/preset-react": "^7.24.7",
5353
"@babel/preset-typescript": "^7.24.7",
54+
"arktype": "^2.1.15",
5455
"babel-plugin-module-resolver": "^5.0.2",
5556
"browserslist": "^4.20.4",
5657
"cross-spawn": "^7.0.3",
Lines changed: 29 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1+
import { type } from 'arktype';
12
import fs from 'fs-extra';
23
import kleur from 'kleur';
34
import path from 'path';
45
import yargs from 'yargs';
5-
import { type Options, type Target } from './types';
6+
import { config, type Config, type Target, type TargetOptions } from './schema';
67
import { loadConfig } from './utils/loadConfig';
78
import * as logger from './utils/logger';
89
import { run } from './utils/workerize';
@@ -39,46 +40,21 @@ export async function build(argv: Argv) {
3940
);
4041
}
4142

42-
const options: Options = result!.config;
43+
const parsed = config(result.config);
4344

44-
if (!options.targets?.length) {
45+
if (parsed instanceof type.errors) {
4546
throw new Error(
46-
`No 'targets' found in the configuration in '${path.relative(
47-
root,
48-
result!.filepath
49-
)}'.`
50-
);
51-
}
52-
53-
const source = options.source;
54-
55-
if (!source) {
56-
throw new Error(
57-
`No 'source' option found in the configuration in '${path.relative(
58-
root,
59-
result!.filepath
60-
)}'.`
47+
`Invalid configuration in ${result.filepath}: ${parsed.summary}`
6148
);
6249
}
6350

64-
const output = options.output;
51+
const { source, output, targets, exclude } = parsed;
6552

66-
if (!output) {
67-
throw new Error(
68-
`No 'output' option found in the configuration in '${path.relative(
69-
root,
70-
result!.filepath
71-
)}'.`
72-
);
73-
}
74-
75-
const exclude = options.exclude ?? '**/{__tests__,__fixtures__,__mocks__}/**';
76-
77-
const commonjs = options.targets?.some((t) =>
53+
const commonjs = targets.some((t) =>
7854
Array.isArray(t) ? t[0] === 'commonjs' : t === 'commonjs'
7955
);
8056

81-
const module = options.targets?.some((t) =>
57+
const module = targets.some((t) =>
8258
Array.isArray(t) ? t[0] === 'module' : t === 'module'
8359
);
8460

@@ -94,68 +70,69 @@ export async function build(argv: Argv) {
9470
source,
9571
output,
9672
exclude,
97-
options,
73+
config: parsed,
9874
variants,
9975
});
10076
} else {
10177
await Promise.all(
102-
options.targets?.map((target) =>
78+
targets.map((target) =>
10379
buildTarget({
10480
root,
105-
target,
81+
target: Array.isArray(target) ? target[0] : target,
10682
source,
10783
output,
10884
exclude,
109-
options,
85+
config: parsed,
11086
variants,
11187
})
11288
)
11389
);
11490
}
11591
}
11692

117-
async function buildTarget({
93+
async function buildTarget<T extends Target>({
11894
root,
11995
target,
12096
source,
12197
output,
12298
exclude,
123-
options,
99+
config,
124100
variants,
125101
}: {
126102
root: string;
127-
target: Exclude<Options['targets'], undefined>[number];
103+
target: T;
128104
source: string;
129105
output: string;
130106
exclude: string;
131-
options: Options;
107+
config: Config;
132108
variants: {
133109
commonjs?: boolean;
134110
module?: boolean;
135111
};
136112
}) {
137-
const targetName = Array.isArray(target) ? target[0] : target;
138-
const targetOptions = Array.isArray(target) ? target[1] : undefined;
113+
const options = config.targets
114+
.map((t) => (Array.isArray(t) ? t : ([t, undefined] as const)))
115+
.find((t) => t[0] === target)?.[1];
139116

140-
const report = logger.grouped(targetName);
117+
const report = logger.grouped(target);
141118

142-
switch (targetName) {
119+
switch (target) {
143120
case 'commonjs':
144121
case 'module':
145-
await run(targetName, {
122+
await run(target, {
146123
root,
147124
source: path.resolve(root, source),
148-
output: path.resolve(root, output, targetName),
125+
output: path.resolve(root, output, target),
149126
exclude,
150-
options: targetOptions,
127+
options: options as TargetOptions<'commonjs' | 'module'>,
151128
variants,
152129
report,
153130
});
154131
break;
155132
case 'typescript':
156133
{
157134
const esm =
158-
options.targets?.some((t) => {
135+
config.targets?.some((t) => {
159136
if (Array.isArray(t)) {
160137
const [name, options] = t;
161138

@@ -171,7 +148,7 @@ async function buildTarget({
171148
root,
172149
source: path.resolve(root, source),
173150
output: path.resolve(root, output, 'typescript'),
174-
options: targetOptions,
151+
options: options as TargetOptions<'typescript'>,
175152
esm,
176153
variants,
177154
report,
@@ -182,19 +159,18 @@ async function buildTarget({
182159
await run('codegen', {
183160
root,
184161
source: path.resolve(root, source),
185-
output: path.resolve(root, output, 'typescript'),
186162
report,
187163
});
188164
break;
189165
case 'custom':
190166
await run('custom', {
191-
options: targetOptions,
167+
root,
192168
source: path.resolve(root, source),
169+
options: options as TargetOptions<'custom'>,
193170
report,
194-
root,
195171
});
196172
break;
197173
default:
198-
throw new Error(`Invalid target ${kleur.blue(targetName)}.`);
174+
throw new Error(`Invalid target ${kleur.blue(target)}.`);
199175
}
200176
}

packages/react-native-builder-bob/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import yargs from 'yargs';
22
import { build } from './build';
33
import { init } from './init';
4-
import type { Target } from './types';
4+
import type { Target } from './schema';
55

66
type ArgName = 'target';
77

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { type } from 'arktype';
2+
3+
const module = {
4+
name: '"module"',
5+
options: type({
6+
esm: type('boolean').default(false),
7+
babelrc: type('boolean').default(false),
8+
configFile: type('boolean').default(false),
9+
sourceMaps: type('boolean').default(true),
10+
copyFlow: type('boolean').default(false),
11+
jsxRuntime: type('"automatic" | "classic"').default('automatic'),
12+
}),
13+
} as const;
14+
15+
const commonjs = {
16+
name: '"commonjs"',
17+
options: module.options,
18+
} as const;
19+
20+
const typescript = {
21+
name: '"typescript"',
22+
options: type({
23+
project: 'string?',
24+
tsc: 'string?',
25+
}),
26+
} as const;
27+
28+
const codegen = {
29+
name: '"codegen"',
30+
} as const;
31+
32+
const custom = {
33+
name: '"custom"',
34+
options: type({
35+
script: 'string',
36+
clean: 'string?',
37+
}),
38+
} as const;
39+
40+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
41+
const target = type.or(
42+
commonjs.name,
43+
module.name,
44+
typescript.name,
45+
codegen.name,
46+
custom.name
47+
);
48+
49+
export const config = type({
50+
source: 'string',
51+
output: 'string',
52+
targets: type
53+
.or(
54+
type.or(module.name, [module.name], [module.name, module.options]),
55+
type.or(
56+
commonjs.name,
57+
[commonjs.name],
58+
[commonjs.name, commonjs.options]
59+
),
60+
type.or(
61+
typescript.name,
62+
[typescript.name],
63+
[typescript.name, typescript.options]
64+
),
65+
type.or(codegen.name, [codegen.name]),
66+
[custom.name, custom.options]
67+
)
68+
.array()
69+
.moreThanLength(0),
70+
exclude: type.string.default('**/{__tests__,__fixtures__,__mocks__}/**'),
71+
}).onDeepUndeclaredKey('reject');
72+
73+
export type Config = typeof config.infer;
74+
75+
export type Target = typeof target.infer;
76+
77+
export type TargetOptions<T extends Target> = T extends typeof commonjs.name
78+
? typeof commonjs.options.infer
79+
: T extends typeof module.name
80+
? typeof module.options.infer
81+
: T extends typeof typescript.name
82+
? typeof typescript.options.infer
83+
: T extends typeof custom.name
84+
? typeof custom.options.infer
85+
: T extends typeof codegen.name
86+
? undefined
87+
: never;

packages/react-native-builder-bob/src/targets/codegen/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import del from 'del';
77
import { runRNCCli } from '../../utils/runRNCCli';
88
import { removeCodegenAppLevelCode } from './patches/removeCodegenAppLevelCode';
99

10-
type Options = Input;
10+
type Options = Omit<Input, 'output'>;
1111

1212
export default async function build({ root, report }: Options) {
1313
const packageJsonPath = path.resolve(root, 'package.json');

packages/react-native-builder-bob/src/types.ts

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export type Log = (message: string) => void;
2+
23
export type Report = {
34
info: Log;
45
warn: Log;
@@ -13,20 +14,6 @@ export type Input = {
1314
report: Report;
1415
};
1516

16-
export type Target =
17-
| 'commonjs'
18-
| 'module'
19-
| 'typescript'
20-
| 'codegen'
21-
| 'custom';
22-
23-
export type Options = {
24-
source?: string;
25-
output?: string;
26-
targets?: (Target | [target: Target, options: object])[];
27-
exclude?: string;
28-
};
29-
3017
export type Variants = {
3118
commonjs?: boolean;
3219
module?: boolean;

packages/react-native-builder-bob/src/utils/loadConfig.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ const searchPlaces = [
88
'package.json',
99
];
1010

11-
export const loadConfig = (root: string) => {
11+
export const loadConfig = (
12+
root: string
13+
): { filepath: string; config: unknown } | undefined => {
1214
for (const filename of searchPlaces) {
1315
const result = requireConfig(root, filename);
1416

packages/react-native-builder-bob/src/utils/workerize.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ import commonjs from '../targets/commonjs';
99
import custom from '../targets/custom';
1010
import module from '../targets/module';
1111
import typescript from '../targets/typescript';
12-
import type { Report, Target } from '../types';
12+
import type { Report } from '../types';
13+
import type { Target } from '../schema';
1314

1415
type WorkerData<T extends Target> = {
1516
target: T;

yarn.lock

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,22 @@ __metadata:
2222
languageName: node
2323
linkType: hard
2424

25+
"@ark/schema@npm:0.45.5":
26+
version: 0.45.5
27+
resolution: "@ark/schema@npm:0.45.5"
28+
dependencies:
29+
"@ark/util": 0.45.5
30+
checksum: d4cd8f3e67f785f1a499e966bc9d10d43597a2dee26ae3226c681d9dfa5e88c8229c989b30703b785fa009eeef7158c54e52ca4bdebe67940e9bb0295d03a185
31+
languageName: node
32+
linkType: hard
33+
34+
"@ark/util@npm:0.45.5":
35+
version: 0.45.5
36+
resolution: "@ark/util@npm:0.45.5"
37+
checksum: 2074b5c0055b3ac857e33c6ffd26463cdd2813aad46b280de8238edf2b7e8a7264e03e73c2e2890cfe2af589742fb982ebaee3f60605238a14b0eb0a0262eb14
38+
languageName: node
39+
linkType: hard
40+
2541
"@babel/cli@npm:^7.24.8":
2642
version: 7.24.8
2743
resolution: "@babel/cli@npm:7.24.8"
@@ -4448,6 +4464,16 @@ __metadata:
44484464
languageName: node
44494465
linkType: hard
44504466

4467+
"arktype@npm:^2.1.15":
4468+
version: 2.1.15
4469+
resolution: "arktype@npm:2.1.15"
4470+
dependencies:
4471+
"@ark/schema": 0.45.5
4472+
"@ark/util": 0.45.5
4473+
checksum: 2f407713f42a7976362c5394435c841b9913e32722c8e0cbf2313a5931a5e7dad64b1efc8fbcac79fd4e7a58be22ab68b6b9c07b7f19abb65862ed548429006a
4474+
languageName: node
4475+
linkType: hard
4476+
44514477
"array-differ@npm:^3.0.0":
44524478
version: 3.0.0
44534479
resolution: "array-differ@npm:3.0.0"
@@ -12723,6 +12749,7 @@ __metadata:
1272312749
"@types/prompts": ^2.0.14
1272412750
"@types/which": ^2.0.1
1272512751
"@types/yargs": ^17.0.10
12752+
arktype: ^2.1.15
1272612753
babel-plugin-module-resolver: ^5.0.2
1272712754
browserslist: ^4.20.4
1272812755
concurrently: ^7.2.2

0 commit comments

Comments
 (0)