-
Notifications
You must be signed in to change notification settings - Fork 15
Data and Type's Strategy
To get good separation, and easier orientation in the code dependencies, naming of the core exposed types is essential. We try to distinguish between 2 main types exposed by packages to describe relations and responsibilities, "strict" and "un-strict"
The Flags
naming is used to define values consumed over the process arguments. These values are wide and normally accept a number of overloads.
In many cases they represent a less strict type of the needed core arguments to execute a process or execute a method directly.
e.g. npx code-pushup --verbose
or for a specific command e.g. npx code-pushup collect --onlyPlugins=eslint,npm-audit
type SeperatedString = string; // e.g "a b c" or "a,b,c"
type MyFlags = {
runs?: number;
'upload.server'?: string;
'only-plugins'?: string | SeperatedString | string[]
onlyPlugins?: string | SeperatedString | string[]
};
The Raw
type is similar to Flags
but instead of describing process argument inputs it describes un-validated or parsed data.
The In
type is similar to Flags
but it describes user input over the terminal (stdin
).
The Options
naming is used to define values consumed by the core logic. These types are most strict and can be trusted.
They could also be used by consumers directly without any interaction with the CLI.
As this data is consumed over terminal input or the file system, this data needs to get validated before forwarded into the core logic.
type MyOptions = {
runs: number;
upload: {
server: string
};
onlyPlugins: string[]
};
Intermediate types are not part of the public API. In best this types are encapsulated into parser and validation logic. They should be specifically designed for certain situations or usecase and not much reused across the program.
Parsing/validation should be used when data is coming from an unknown source:
- parsing/validating
ConfigRaw
fromrc.config.json
toConfigOptions
- normalizing
VerboseFlag
toDebugOptions
- validating prompt input (
stdin
) toOnlyPlugins
It is always done as early as possible and preferably on the most outer edge of the program.
In our example we use zod
as parser.
The 3 main packages models
, core
, cli
encapsulate a specific responsibility:
-
model
- maintain types and parser for the logic incore
.
Types used here are all kind ofOption
's as well as other more strict intermediate types. -
core
- reuse final typing. All less strict types should be maintained incli
oth other consumer ofcore
.
Types exposed incore
are most strict and alwaysOption
types. -
cli
- reusesmodel
and maintains all validations and parsing logic for less strict types. It executes thecore
logic. Types exposed here are less strict likeFlags
,Raw
orIn
. Most of the less strict types are turned to strict types in this layer.
flowchart LR
Model
Core --> Model
Cli --> Model
Cli --> Core
// === @code-pushup/model
type PersistOptions = {
persist: {
filename: string
}
};
type PluginOptions = {
plugins: string[]
};
// === @code-pushup/core
type GlobalOptions = {
verbose: boolean
};
type CollectOnlyOptions = {
onlyPlugins: string[]
};
type CollectOptions = GlobalOptions & PluginOptions & PersistOptions & CollectOnlyOptions;
type CollectLogic = (options: CollectOptions) => unknown;
// === @code-pushup/cli
// {persist: {filename: string}} => {persist?: {filename?: string}}
type DeepPartial<T> = { [Key in keyof T]?: Partial<T[Key]> };
// {persist: {filename: string}} => {'persist.filename': string}
type DotNotation<T> = { [Key in keyof T]?: Partial<T[Key]> };
type GlobalFlags = Partial<GlobalOptions>;
// Config
type ConfigFlags = { configPath?: string } & Partial<DotNotation<PersistOptions>>;
// --> Inside Middleware
type ConfigRaw = PluginOptions & DeepPartial<PersistOptions>;
// GlobalFlags to GlobalOptions
// ConfigFlags to DeepPartial<PersistOptions>
// GlobalFlags.configPath to ConfigRaw
// zod(DeepPartial<PersistOptions> & ConfigRaw & DEFAULTS): ConfigOptions
type ConfigMiddleware<T extends GlobalFlags & ConfigFlags & DEFAULTS> = (args: T) => GlobalOptions & ConfigOptions;
// ---|
// Collect
type SeperatedString = string; // e.g "a b c" or "a,b,c"
type CollectFlags = {
onlyPlugins?: SeperatedString | string | string[]
};
// --> Inside Middleware
// CollectFlags to CollectOnlyOptions
// ConfigOptions to PluginOptions & PersistOptions
// zod(PluginOptions & PersistOptions & DEFAULTS_OR_ERROR): CollectOptions
type CollectMiddleware<T extends GlobalOptions & ConfigOptions & CollectFlags> = (args: T) => GlobalOptions & CollectOptions;
// ---|
// Call collectLogic