-
Couldn't load subscription status.
- Fork 16
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
ConfigRawfromrc.config.jsontoConfigOptions - normalizing
VerboseFlagtoDebugOptions - 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 inclioth other consumer ofcore.
Types exposed incoreare most strict and alwaysOptiontypes. -
cli- reusesmodeland maintains all validations and parsing logic for less strict types. It executes thecorelogic. Types exposed here are less strict likeFlags,RaworIn. 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 ConfigOptions = PluginOptions & PersistOptions;
// GlobalFlags to GlobalOptions
// ConfigFlags to DeepPartial<PersistOptions>
// GlobalFlags.configPath to ConfigRaw (PluginOptions & DeepPartial<PersistOptions>)
// zod(DeepPartial<PersistOptions> & ConfigRaw & DEFAULTS): ConfigOptions
type ConfigMiddleware<T extends GlobalFlags & ConfigFlags> = (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 = (args: GlobalOptions & ConfigOptions & CollectFlags) => GlobalOptions & CollectOptions;
// ---|
// Call collectLogic