feat: add autoConfig/ better types for wrapperData

This commit is contained in:
BluJ
2023-04-19 17:23:16 -06:00
parent 7c4f94ba8f
commit e279711f8e
39 changed files with 431 additions and 291 deletions

View File

@@ -1,70 +1,42 @@
import deepmerge from "deepmerge";
import { AutoConfigure, Effects } from "../types";
import { Message, MaybePromise, ReadonlyDeep } from ".";
import { AutoConfigure, Effects, ExpectedExports } from "../types";
import { deepEqual, deepMerge } from "../util";
class AutoConfigBuilt<Config> implements AutoConfigure<Config> {
constructor(private autoConfig: AutoConfig<Config>) {}
async check(effects: Effects, config: Config): Promise<void> {
for (const [message, configure] of this.autoConfig.getConfigures()) {
const value = await configure({ effects, config });
if (value !== null) {
throw new Error(message);
}
}
}
/** This is called after we know that the dependency package needs a new configuration, this would be a transform for defaults */
async autoConfigure(effects: Effects, config: Config): Promise<Config> {
const input = { effects, config };
const newOverwrites = (
await Promise.all(this.autoConfig.getConfigures().map((x) => x[1](input)))
).filter((x): x is NonNullable<typeof x> => x !== null);
return deepmerge.all([config, ...newOverwrites]);
}
}
export class AutoConfig<Config> {
private constructor(
private configures: Array<
[
Message,
(
options: Readonly<{ config: Config; effects: Effects }>
) => MaybePromise<null | Partial<Config>>
]
>
export type AutoConfigFrom = {
[key: string]: (options: {
effects: Effects;
localConfig: unknown;
remoteConfig: unknown;
}) => Promise<void | Record<string, unknown>>;
};
export class AutoConfig {
constructor(
readonly configs: AutoConfigFrom,
readonly path: keyof AutoConfigFrom,
) {}
getConfigures(): ReadonlyDeep<
Array<
[
Message,
(
options: Readonly<{ config: Config; effects: Effects }>
) => MaybePromise<null | Partial<Config>>
]
>
> {
return this.configures;
}
static autoConfig<Config>(
message: Message,
configure: (
options: Readonly<{ config: Config; effects: Effects }>
) => MaybePromise<null | Partial<Config>>
): AutoConfig<Config> {
return new AutoConfig([[message, configure]]);
async check(
options: Parameters<AutoConfigure["check"]>[0],
): ReturnType<AutoConfigure["check"]> {
const origConfig = JSON.parse(JSON.stringify(options.localConfig));
if (
!deepEqual(
origConfig,
deepMerge(
{},
options.localConfig,
await this.configs[this.path](options),
),
)
)
throw new Error(`Check failed for ${this.path}`);
}
autoConfig(
message: Message,
configure: (
options: Readonly<{ config: Config; effects: Effects }>
) => MaybePromise<null | Partial<Config>>
): AutoConfig<Config> {
this.configures.push([message, configure]);
return this;
}
build() {
return new AutoConfigBuilt(this);
async autoConfigure(
options: Parameters<AutoConfigure["autoConfigure"]>[0],
): ReturnType<AutoConfigure["autoConfigure"]> {
return deepMerge(
{},
options.localConfig,
await this.configs[this.path](options),
);
}
}

View File

@@ -1,14 +0,0 @@
import { ExpectedExports, PackageId } from "../types";
import { AutoConfig } from "./AutoConfig";
export function autoconfigSetup<Config>(
autoconfigs: Record<PackageId, AutoConfig<Config>>
) {
const autoconfig: ExpectedExports.autoConfig<Config> = {};
for (const [id, autoconfigValue] of Object.entries(autoconfigs)) {
autoconfig[id] = autoconfigValue.build();
}
return autoconfig;
}

View File

@@ -6,4 +6,4 @@ export type MaybePromise<A> = Promise<A> | A;
export type Message = string;
export { AutoConfig } from "./AutoConfig";
export { autoconfigSetup } from "./autoconfigSetup";
export { setupAutoConfig } from "./setupAutoConfig";

View File

@@ -0,0 +1,9 @@
import { AutoConfig, AutoConfigFrom } from "./AutoConfig";
export function setupAutoConfig<C extends AutoConfigFrom>(configs: C) {
const answer = { ...configs } as unknown as { [k in keyof C]: AutoConfig };
for (const key in configs) {
answer[key] = new AutoConfig(configs, key);
}
return answer;
}

View File

@@ -40,7 +40,7 @@ export class Backups {
constructor(
private options = DEFAULT_OPTIONS,
private backupSet = [] as BackupSet[]
private backupSet = [] as BackupSet[],
) {}
static volumes(...volumeNames: string[]) {
return new Backups().addSets(
@@ -49,7 +49,7 @@ export class Backups {
srcPath: "./",
dstPath: `./${srcVolume}/`,
dstVolume: Backups.BACKUP,
}))
})),
);
}
static addSets(...options: BackupSet[]) {
@@ -72,12 +72,12 @@ export class Backups {
srcPath: "./",
dstPath: `./${srcVolume}/`,
dstVolume: Backups.BACKUP,
}))
})),
);
}
addSets(...options: BackupSet[]) {
options.forEach((x) =>
this.backupSet.push({ ...x, options: { ...this.options, ...x.options } })
this.backupSet.push({ ...x, options: { ...this.options, ...x.options } }),
);
return this;
}
@@ -98,7 +98,7 @@ export class Backups {
.map((x) => x.dstPath)
.map((x) => x.replace(/\.\/([^]*)\//, "$1"));
const filteredItems = previousItems.filter(
(x) => backupPaths.indexOf(x) === -1
(x) => backupPaths.indexOf(x) === -1,
);
for (const itemToRemove of filteredItems) {
effects.error(`Trying to remove ${itemToRemove}`);
@@ -111,7 +111,7 @@ export class Backups {
effects.removeFile({
volumeId: Backups.BACKUP,
path: itemToRemove,
})
}),
)
.catch(() => {
effects.warn(`Failed to remove ${itemToRemove} from backup volume`);

View File

@@ -65,13 +65,13 @@ export class Config<A extends InputSpec> extends IBuilder<A> {
}
static withValue<K extends string, B extends ValueSpec>(
key: K,
value: Value<B>
value: Value<B>,
) {
return Config.empty().withValue(key, value);
}
static addValue<K extends string, B extends ValueSpec>(
key: K,
value: Value<B>
value: Value<B>,
) {
return Config.empty().withValue(key, value);
}

View File

@@ -40,7 +40,7 @@ export class List<A extends ValueSpecList> extends IBuilder<A> {
patternDescription?: string | null;
/** Default = "text" */
inputmode?: ListValueSpecString["inputmode"];
}
},
) {
const spec = {
type: "string" as const,
@@ -77,7 +77,7 @@ export class List<A extends ValueSpecList> extends IBuilder<A> {
range?: string;
units?: string | null;
placeholder?: string | null;
}
},
) {
const spec = {
type: "number" as const,
@@ -111,7 +111,7 @@ export class List<A extends ValueSpecList> extends IBuilder<A> {
spec: Spec;
displayAs?: null | string;
uniqueBy?: null | UniqueBy;
}
},
) {
const { spec: previousSpecSpec, ...restSpec } = aSpec;
const specSpec = previousSpecSpec.build() as BuilderExtract<Spec>;

View File

@@ -154,7 +154,7 @@ export class Value<A extends ValueSpec> extends IBuilder<A> {
description?: string | null;
warning?: string | null;
},
previousSpec: Spec
previousSpec: Spec,
) {
const spec = previousSpec.build() as BuilderExtract<Spec>;
return new Value({
@@ -166,7 +166,7 @@ export class Value<A extends ValueSpec> extends IBuilder<A> {
});
}
static union<
V extends Variants<{ [key: string]: { name: string; spec: InputSpec } }>
V extends Variants<{ [key: string]: { name: string; spec: InputSpec } }>,
>(
a: {
name: string;
@@ -175,7 +175,7 @@ export class Value<A extends ValueSpec> extends IBuilder<A> {
required: boolean;
default?: string | null;
},
aVariants: V
aVariants: V,
) {
const variants = aVariants.build() as BuilderExtract<V>;
return new Value({

View File

@@ -57,12 +57,12 @@ export class Variants<
name: string;
spec: InputSpec;
};
}
},
> extends IBuilder<A> {
static of<
A extends {
[key: string]: { name: string; spec: Config<InputSpec> };
}
},
>(a: A) {
const variants: {
[K in keyof A]: { name: string; spec: BuilderExtract<A[K]["spec"]> };
@@ -82,7 +82,7 @@ export class Variants<
}
static withVariant<K extends string, B extends InputSpec>(
key: K,
value: Config<B>
value: Config<B>,
) {
return Variants.empty().withVariant(key, value);
}

View File

@@ -160,7 +160,7 @@ export type DefaultString =
// sometimes the type checker needs just a little bit of help
export function isValueSpecListOf<S extends ListValueSpecType>(
t: ValueSpecListOf<ListValueSpecType>,
s: S
s: S,
): t is ValueSpecListOf<S> & { spec: ListValueSpecOf<S> } {
return t.spec.type === s;
}

View File

@@ -1,18 +1,25 @@
import { Config } from "./builder";
import { DeepPartial, Dependencies, DependsOn, Effects, ExpectedExports } from "../types";
import {
DeepPartial,
Dependencies,
DependsOn,
Effects,
ExpectedExports,
} from "../types";
import { InputSpec } from "./configTypes";
import { nullIfEmpty } from "../util";
import { TypeFromProps } from "../util/propertiesMatcher";
export type Write<A, ConfigType> = (options: { effects: Effects; input: TypeFromProps<A> }) => Promise<ConfigType>;
export type Read<A, ConfigType> = (options: {
effects: Effects;
config: ConfigType;
}) => Promise<null | DeepPartial<TypeFromProps<A>>>;
export type DependenciesFn<A, ConfigType> = (options: {
export type Write<A> = (options: {
effects: Effects;
input: TypeFromProps<A>;
}) => Promise<void>;
export type Read<A> = (options: {
effects: Effects;
}) => Promise<null | DeepPartial<TypeFromProps<A>>>;
export type DependenciesFn<A> = (options: {
effects: Effects;
input: TypeFromProps<A>;
config: ConfigType;
}) => Promise<Dependencies | void>;
/**
* We want to setup a config export with a get and set, this
@@ -21,31 +28,30 @@ export type DependenciesFn<A, ConfigType> = (options: {
* @param options
* @returns
*/
export function setupConfigExports<A extends InputSpec, ConfigType>(options: {
spec: Config<A>;
write: Write<A, ConfigType>;
read: Read<A, ConfigType>;
dependencies: DependenciesFn<A, ConfigType>;
}) {
const validator = options.spec.validator();
export function setupConfigExports<A extends InputSpec>(
spec: Config<A>,
write: Write<A>,
read: Read<A>,
dependencies: DependenciesFn<A>,
) {
const validator = spec.validator();
return {
setConfig: (async ({ effects, input }) => {
if (!validator.test(input)) {
await effects.error(String(validator.errorMessage(input)));
return { error: "Set config type error for config" };
}
const config = await options.write({
await write({
input: JSON.parse(JSON.stringify(input)),
effects,
});
const dependencies = (await options.dependencies({ effects, input, config })) || [];
await effects.setDependencies(dependencies);
await effects.setWrapperData({ path: "config", value: config || null });
const dependenciesToSet = (await dependencies({ effects, input })) || [];
await effects.setDependencies(dependenciesToSet);
}) as ExpectedExports.setConfig,
getConfig: (async ({ effects, config }) => {
return {
spec: options.spec.build(),
config: nullIfEmpty(await options.read({ effects, config: config as ConfigType })),
spec: spec.build(),
config: nullIfEmpty(await read({ effects })),
};
}) as ExpectedExports.getConfig,
};

View File

@@ -6,15 +6,15 @@ import * as C from "./configTypes";
export async function specToBuilderFile(
file: string,
inputData: Promise<InputSpec> | InputSpec,
options: Parameters<typeof specToBuilder>[1]
options: Parameters<typeof specToBuilder>[1],
) {
await fs.writeFile(file, await specToBuilder(inputData, options), (err) =>
console.error(err)
console.error(err),
);
}
export async function specToBuilder(
inputData: Promise<InputSpec> | InputSpec,
{ startSdk = "start-sdk" } = {}
{ startSdk = "start-sdk" } = {},
) {
const outputLines: string[] = [];
outputLines.push(`
@@ -26,10 +26,10 @@ export async function specToBuilder(
const configName = newConst("InputSpec", convertInputSpec(data));
const configMatcherName = newConst(
"matchInputSpec",
`${configName}.validator()`
`${configName}.validator()`,
);
outputLines.push(
`export type InputSpec = typeof ${configMatcherName}._TYPE;`
`export type InputSpec = typeof ${configMatcherName}._TYPE;`,
);
return outputLines.join("\n");
@@ -73,7 +73,7 @@ export async function specToBuilder(
const { variants, type, ...rest } = value;
const variantVariable = newConst(
value.name + "_variants",
convertVariants(variants)
convertVariants(variants),
);
return `Value.union(${JSON.stringify(rest)}, ${variantVariable})`;
@@ -101,7 +101,7 @@ export async function specToBuilder(
warning: value.warning || null,
},
null,
2
2,
)}, ${JSON.stringify({
masked: spec?.masked || false,
placeholder: spec?.placeholder || null,
@@ -120,7 +120,7 @@ export async function specToBuilder(
warning: value.warning || null,
},
null,
2
2,
)}, ${JSON.stringify({
range: spec?.range || null,
integral: spec?.integral || false,
@@ -131,7 +131,7 @@ export async function specToBuilder(
case "object": {
const specName = newConst(
value.name + "_spec",
convertInputSpec(spec.spec)
convertInputSpec(spec.spec),
);
return `List.obj({
name: ${JSON.stringify(value.name || null)},
@@ -155,7 +155,7 @@ export async function specToBuilder(
name: string;
spec: C.InputSpec;
}
>
>,
): string {
let answer = "Variants.of({";
for (const [key, { name, spec }] of Object.entries(variants)) {

View File

@@ -248,7 +248,7 @@ export class Checker {
* Check is the function that will be given a emver or unparsed emver and should give if it follows
* a pattern
*/
public readonly check: (value: string | EmVer) => boolean
public readonly check: (value: string | EmVer) => boolean,
) {}
/**

View File

@@ -22,7 +22,7 @@ export async function checkPortListening(
{
error = `Port ${port} is not listening`,
message = `Port ${port} is available`,
} = {}
} = {},
): Promise<CheckResult> {
const hasAddress =
containsAddress(await effects.runCommand(`cat /proc/net/tcp`), port) ||

View File

@@ -15,7 +15,7 @@ export const checkWebUrl = async (
timeout = 1000,
successMessage = `Reached ${url}`,
errorMessage = `Error while fetching URL: ${url}`,
} = {}
} = {},
): Promise<CheckResult> => {
return Promise.race([effects.fetch(url), timeoutPromise(timeout)])
.then((x) => ({

View File

@@ -5,7 +5,7 @@ export { checkWebUrl } from "./checkWebUrl";
export function timeoutPromise(ms: number, { message = "Timed out" } = {}) {
return new Promise<never>((resolve, reject) =>
setTimeout(() => reject(new Error(message)), ms)
setTimeout(() => reject(new Error(message)), ms),
);
}
export { runHealthScript };

View File

@@ -17,7 +17,7 @@ export const runHealthScript = async <A extends string>(
errorMessage = `Error while running command: ${runCommand}`,
message = (res: string) =>
`Have ran script ${runCommand} and the result: ${res}`,
} = {}
} = {},
): Promise<CheckResult> => {
const res = await Promise.race([
effects.runCommand(runCommand, { timeoutMillis: timeout }),

View File

@@ -28,7 +28,7 @@ export function noMigrationsDown(): MigrationDownReceipt {
return {} as MigrationDownReceipt;
}
export function migrationDown(
fn: () => Promise<unknown>
fn: () => Promise<unknown>,
): MigrationDownReceipt {
fn();
return {} as MigrationDownReceipt;
@@ -37,7 +37,7 @@ export function migrationDown(
export function setupInit(
fn: (
...args: Parameters<ExpectedExports.init>
) => Promise<[MigrationReceipt, ActionReceipt]>
) => Promise<[MigrationReceipt, ActionReceipt]>,
) {
const initFn: ExpectedExports.init = (...args) => fn(...args);
return initFn;
@@ -46,7 +46,7 @@ export function setupInit(
export function setupUninit(
fn: (
...args: Parameters<ExpectedExports.uninit>
) => Promise<[MigrationDownReceipt]>
) => Promise<[MigrationDownReceipt]>,
) {
const uninitFn: ExpectedExports.uninit = (...args) => fn(...args);
return uninitFn;

View File

@@ -7,7 +7,7 @@ import { InterfaceReceipt } from "./interfaceReceipt";
type Daemon<
Ids extends string | never,
Command extends string,
Id extends string
Id extends string,
> = {
id: Id;
command: ValidIfNoStupidEscape<Command> | [string, ...string[]];
@@ -52,7 +52,7 @@ export class Daemons<Ids extends string | never> {
private constructor(
readonly effects: Effects,
readonly started: (onTerm: () => void) => null,
readonly daemons?: Daemon<Ids, "command", Ids>[]
readonly daemons?: Daemon<Ids, "command", Ids>[],
) {}
static of(config: {
@@ -64,7 +64,7 @@ export class Daemons<Ids extends string | never> {
return new Daemons<never>(config.effects, config.started);
}
addDaemon<Id extends string, Command extends string>(
newDaemon: Daemon<Ids, Command, Id>
newDaemon: Daemon<Ids, Command, Id>,
) {
const daemons = ((this?.daemons ?? []) as any[]).concat(newDaemon);
return new Daemons<Ids | Id>(this.effects, this.started, daemons);
@@ -76,7 +76,7 @@ export class Daemons<Ids extends string | never> {
const daemons = this.daemons ?? [];
for (const daemon of daemons) {
const requiredPromise = Promise.all(
daemon.requires?.map((id) => daemonsStarted[id]) ?? []
daemon.requires?.map((id) => daemonsStarted[id]) ?? [],
);
daemonsStarted[daemon.id] = requiredPromise.then(async () => {
const { command } = daemon;
@@ -100,15 +100,15 @@ export class Daemons<Ids extends string | never> {
async term() {
await Promise.all(
Object.values<Promise<DaemonReturned>>(daemonsStarted).map((x) =>
x.then((x) => x.term())
)
x.then((x) => x.term()),
),
);
},
async wait() {
await Promise.all(
Object.values<Promise<DaemonReturned>>(daemonsStarted).map((x) =>
x.then((x) => x.wait())
)
x.then((x) => x.wait()),
),
);
},
};

View File

@@ -13,7 +13,7 @@ export class NetworkInterfaceBuilder {
basic?: null | { password: string; username: string };
path?: string;
search?: Record<string, string>;
}
},
) {}
async exportAddresses(addresses: Iterable<Origin>) {

View File

@@ -8,7 +8,7 @@ export class Origin {
username: string;
}
| null
| undefined
| undefined,
) {
// prettier-ignore
const urlAuth = !!(origin) ? `${origin.username}:${origin.password}@` :

View File

@@ -25,7 +25,7 @@ export const runningMain: (
fn: (o: {
effects: Effects;
started(onTerm: () => void): null;
}) => Promise<Daemons<any>>
}) => Promise<Daemons<any>>,
) => ExpectedExports.main = (fn) => {
return async (options) => {
const result = await fn(options);

View File

@@ -21,7 +21,7 @@ export type UnionToIntersection<T> = ((x: T) => any) extends (x: infer R) => any
export function setupPropertiesExport(
fn: (
...args: Parameters<ExpectedExports.properties>
) => void | Promise<void> | Promise<(PropertyGroup | PropertyString)[]>
) => void | Promise<void> | Promise<(PropertyGroup | PropertyString)[]>,
): ExpectedExports.properties {
return (async (...args) => {
const result = await fn(...args);

View File

@@ -43,7 +43,7 @@ describe("builder tests", () => {
}}`
.replaceAll("\n", " ")
.replaceAll(/\s{2,}/g, "")
.replaceAll(": ", ":")
.replaceAll(": ", ":"),
);
});
});
@@ -121,7 +121,7 @@ describe("values", () => {
a: Value.boolean({
name: "test",
}),
})
}),
);
const validator = value.validator();
validator.unsafeCast({ a: true });
@@ -138,7 +138,7 @@ describe("values", () => {
name: "a",
spec: Config.of({ b: Value.boolean({ name: "b" }) }),
},
})
}),
);
const validator = value.validator();
validator.unsafeCast({ unionSelectKey: "a", unionValueKey: { b: false } });
@@ -156,8 +156,8 @@ describe("values", () => {
},
{
integral: false,
}
)
},
),
);
const validator = value.validator();
validator.unsafeCast([1, 2, 3]);
@@ -174,8 +174,8 @@ describe("Builder List", () => {
},
{
spec: Config.of({ test: Value.boolean({ name: "test" }) }),
}
)
},
),
);
const validator = value.validator();
validator.unsafeCast([{ test: true }]);
@@ -187,8 +187,8 @@ describe("Builder List", () => {
{
name: "test",
},
{}
)
{},
),
);
const validator = value.validator();
validator.unsafeCast(["test", "text"]);
@@ -200,8 +200,8 @@ describe("Builder List", () => {
{
name: "test",
},
{ integral: true }
)
{ integral: true },
),
);
const validator = value.validator();
validator.unsafeCast([12, 45]);

View File

@@ -21,7 +21,7 @@ describe("Config Types", () => {
someList.spec satisfies ListValueSpecOf<"object">;
} else {
throw new Error(
"Failed to figure out the type: " + JSON.stringify(someList)
"Failed to figure out the type: " + JSON.stringify(someList),
);
}
}

View File

@@ -407,5 +407,5 @@ writeConvertedFile(
},
{
startSdk: "../",
}
},
);

View File

@@ -20,7 +20,7 @@ function isObject(item: unknown): item is object {
return !!(item && typeof item === "object" && !Array.isArray(item));
}
type UnionToIntersection<T> = (T extends any ? (x: T) => any : never) extends (
x: infer R
x: infer R,
) => any
? R
: never;
@@ -61,14 +61,14 @@ testOutput<
>()(null);
testOutput<InputSpec["rpc"]["advanced"]["servertimeout"], number>()(null);
testOutput<InputSpec["advanced"]["peers"]["addnode"][0]["hostname"], string>()(
null
null,
);
testOutput<
InputSpec["testListUnion"][0]["union"][UnionValueKey]["name"],
string
>()(null);
testOutput<InputSpec["testListUnion"][0]["union"][UnionSelectKey], "lnd">()(
null
null,
);
// prettier-ignore
// @ts-expect-error Expect that the string is the one above
@@ -139,19 +139,19 @@ describe("Inputs", () => {
test("test errors", () => {
expect(() =>
matchInputSpec.unsafeCast(
mergeDeep(validInput, { rpc: { advanced: { threads: 0 } } })
)
mergeDeep(validInput, { rpc: { advanced: { threads: 0 } } }),
),
).toThrowError();
expect(() =>
matchInputSpec.unsafeCast(mergeDeep(validInput, { rpc: { enable: 2 } }))
matchInputSpec.unsafeCast(mergeDeep(validInput, { rpc: { enable: 2 } })),
).toThrowError();
expect(() =>
matchInputSpec.unsafeCast(
mergeDeep(validInput, {
rpc: { advanced: { serialversion: "testing" } },
})
)
}),
),
).toThrowError();
});
});

View File

@@ -0,0 +1,81 @@
import { T } from "..";
import { utils } from "../util";
type WrapperType = {
config: {
someValue: string;
};
};
const todo = <A>(): A => {
throw new Error("not implemented");
};
const noop = () => {};
describe("wrapperData", () => {
test.skip("types", async () => {
utils<WrapperType>(todo<T.Effects>()).setWrapperData(
"/config/someValue",
"someValue",
);
utils<WrapperType>(todo<T.Effects>()).setWrapperData(
"/config/someValue",
// @ts-expect-error Type is wrong for the setting value
5,
);
utils<WrapperType>(todo<T.Effects>()).setWrapperData(
// @ts-expect-error Path is wrong
"/config/someVae3lue",
"someValue",
);
todo<T.Effects>().setWrapperData<WrapperType, "/config/someValue">({
path: "/config/someValue",
value: "someValueIn",
});
todo<T.Effects>().setWrapperData<WrapperType, "/config/some2Value">({
//@ts-expect-error Path is wrong
path: "/config/someValue",
//@ts-expect-error Path is wrong
value: "someValueIn",
});
todo<T.Effects>().setWrapperData<WrapperType, "/config/someValue">({
//@ts-expect-error Path is wrong
path: "/config/some2Value",
value: "someValueIn",
});
(await utils<WrapperType>(todo<T.Effects>())
.getWrapperData("/config/someValue")
.const()) satisfies string;
(await utils<WrapperType>(todo<T.Effects>())
.getWrapperData("/config")
.const()) satisfies WrapperType["config"];
await utils<WrapperType>(todo<T.Effects>())
// @ts-expect-error Path is wrong
.getWrapperData("/config/somdsfeValue")
.const();
(await utils<WrapperType>(todo<T.Effects>())
.getWrapperData("/config/someValue")
// @ts-expect-error satisfies type is wrong
.const()) satisfies number;
(await utils<WrapperType>(todo<T.Effects>())
// @ts-expect-error Path is wrong
.getWrapperData("/config/")
.const()) satisfies WrapperType["config"];
(await todo<T.Effects>().getWrapperData<WrapperType, "/config/someValue">({
path: "/config/someValue",
callback: noop,
})) satisfies string;
await todo<T.Effects>().getWrapperData<WrapperType, "/config/someValue">({
// @ts-expect-error Path is wrong as in it doesn't match above
path: "/config/someV2alue",
callback: noop,
});
await todo<T.Effects>().getWrapperData<WrapperType, "/config/someV2alue">({
// @ts-expect-error Path is wrong as in it doesn't exists in wrapper type
path: "/config/someV2alue",
callback: noop,
});
});
});

View File

@@ -85,7 +85,7 @@ export namespace ExpectedExports {
/** Auto configure is used to make sure that other dependencies have the values t
* that this service could use.
*/
export type autoConfig<Config> = Record<PackageId, AutoConfigure<Config>>;
export type autoConfig = Record<PackageId, AutoConfigure>;
}
export type TimeMs = number;
export type VersionString = string;
@@ -94,11 +94,19 @@ export type VersionString = string;
* AutoConfigure is used as the value to the key of package id,
* this is used to make sure that other dependencies have the values that this service could use.
*/
export type AutoConfigure<Config> = {
export type AutoConfigure = {
/** Checks are called to make sure that our dependency is in the correct shape. If a known error is returned we know that the dependency needs modification */
check(effects: Effects, input: Config): Promise<void>;
check(options: {
effects: Effects;
localConfig: unknown;
remoteConfig: unknown;
}): Promise<void>;
/** This is called after we know that the dependency package needs a new configuration, this would be a transform for defaults */
autoConfigure(effects: Effects, input: Config): Promise<Config>;
autoConfigure(options: {
effects: Effects;
localConfig: unknown;
remoteConfig: unknown;
}): Promise<unknown>;
};
export type ValidIfNoStupidEscape<A> = A extends
@@ -172,14 +180,14 @@ export type Effects = {
command: ValidIfNoStupidEscape<A> | [string, ...string[]],
input?: {
timeoutMillis?: number;
}
},
): Promise<string>;
runShellDaemon(command: string): {
wait(): Promise<string>;
term(): Promise<void>;
};
runDaemon<A extends string>(
command: ValidIfNoStupidEscape<A> | [string, ...string[]]
command: ValidIfNoStupidEscape<A> | [string, ...string[]],
): DaemonReturned;
/** Uses the chown on the system */
@@ -225,7 +233,7 @@ export type Effects = {
method?: "GET" | "POST" | "PUT" | "DELETE" | "HEAD" | "PATCH";
headers?: Record<string, string>;
body?: string;
}
},
): Promise<{
method: string;
ok: boolean;
@@ -256,19 +264,19 @@ export type Effects = {
};
/** Get a value in a json like data, can be observed and subscribed */
getWrapperData(options: {
getWrapperData<WrapperData = never, Path extends string = never>(options: {
/** If there is no packageId it is assumed the current package */
packageId?: string;
/** The path defaults to root level, using the [JsonPath](https://jsonpath.com/) */
path?: string;
path: Path & EnsureWrapperDataPath<WrapperData, Path>;
callback: (config: unknown, previousConfig: unknown) => void;
}): Promise<unknown>;
}): Promise<ExtractWrapperData<WrapperData, Path>>;
/** Used to store values that can be accessed and subscribed to */
setWrapperData(options: {
setWrapperData<WrapperData = never, Path extends string = never>(options: {
/** Sets the value for the wrapper at the path, it will override, using the [JsonPath](https://jsonpath.com/) */
path?: string;
value: unknown;
path: Path & EnsureWrapperDataPath<WrapperData, Path>;
value: ExtractWrapperData<WrapperData, Path>;
}): Promise<void>;
getLocalHostname(): Promise<string>;
@@ -276,14 +284,14 @@ export type Effects = {
/** Get the address for another service for tor interfaces */
getServiceTorHostname(
interfaceId: string,
packageId?: string
packageId?: string,
): Promise<string>;
/**
* Get the port address for another service
*/
getServicePortForward(
internalPort: number,
packageId?: string
packageId?: string,
): Promise<number>;
/** When we want to create a link in the front end interfaces, and example is
@@ -356,7 +364,7 @@ export type Effects = {
*/
getSslCertificate: (
packageId: string,
algorithm?: "ecdsa" | "ed25519"
algorithm?: "ecdsa" | "ed25519",
) => [string, string, string];
/**
* @returns PEM encoded ssl key (ecdsa)
@@ -379,6 +387,20 @@ export type Effects = {
shutdown(): void;
};
// prettier-ignore
export type ExtractWrapperData<WrapperData, Path extends string> =
Path extends `/${infer A }/${infer Rest }` ? (A extends keyof WrapperData ? ExtractWrapperData<WrapperData[A], `/${Rest}`> : never) :
Path extends `/${infer A }` ? (A extends keyof WrapperData ? WrapperData[A] : never) :
never
// prettier-ignore
type _EnsureWrapperDataPath<WrapperData, Path extends string, Origin extends string> =
Path extends `/${infer A }/${infer Rest }` ? (A extends keyof WrapperData ? ExtractWrapperData<WrapperData[A], `/${Rest}`> : never) :
Path extends `/${infer A }` ? (A extends keyof WrapperData ? Origin : never) :
never
// prettier-ignore
export type EnsureWrapperDataPath<WrapperData, Path extends string> = _EnsureWrapperDataPath<WrapperData, Path, Path>
/* rsync options: https://linux.die.net/man/1/rsync
*/
export type BackupOptions = {

19
lib/util/deepEqual.ts Normal file
View File

@@ -0,0 +1,19 @@
import { object } from "ts-matches";
export function deepEqual(...args: unknown[]) {
if (!object.test(args[args.length - 1])) return args[args.length - 1];
const objects = args.filter(object.test);
if (objects.length === 0) {
for (const x of args) if (x !== args[0]) return false;
return true;
}
if (objects.length !== args.length) return false;
const allKeys = new Set(objects.flatMap((x) => Object.keys(x)));
for (const key of allKeys) {
for (const x of objects) {
if (!(key in x)) return false;
if (!deepEqual((objects[0] as any)[key], (x as any)[key])) return false;
}
}
return true;
}

16
lib/util/deepMerge.ts Normal file
View File

@@ -0,0 +1,16 @@
import { object } from "ts-matches";
export function deepMerge(...args: unknown[]): unknown {
const lastItem = (args as any)[args.length - 1];
if (!object.test(lastItem)) return lastItem;
const objects = args.filter(object.test).filter((x) => !Array.isArray(x));
if (objects.length === 0) return lastItem as any;
const allKeys = new Set(objects.flatMap((x) => Object.keys(x)));
for (const key of allKeys) {
const filteredValues = objects.flatMap((x) =>
key in x ? [(x as any)[key]] : [],
);
(objects as any)[0][key] = deepMerge(...filteredValues);
}
return objects[0] as any;
}

View File

@@ -8,7 +8,7 @@ type UnReadonly<A> = { -readonly [k in keyof A]: A[k] };
declare global {
interface Object {
entries<T extends {}>(
this: T
this: T,
): Array<{ -readonly [K in keyof T]: [K, T[K]] }[keyof T]>;
values<T extends {}>(this: T): Array<T[keyof T]>;
keys<T extends {}>(this: T): Array<keyof T>;

View File

@@ -56,7 +56,7 @@ export class FileHelper<A> {
readonly path: string,
readonly volume: string,
readonly writeData: (dataIn: A) => string,
readonly readData: (stringValue: string) => A
readonly readData: (stringValue: string) => A,
) {}
async write(data: A, effects: T.Effects) {
let matched;
@@ -86,21 +86,21 @@ export class FileHelper<A> {
await effects.readFile({
path: this.path,
volumeId: this.volume,
})
}),
);
}
static raw<A>(
path: string,
volume: string,
toFile: (dataIn: A) => string,
fromFile: (rawData: string) => A
fromFile: (rawData: string) => A,
) {
return new FileHelper<A>(path, volume, toFile, fromFile);
}
static json<A>(
path: string,
volume: string,
shape: matches.Validator<unknown, A>
shape: matches.Validator<unknown, A>,
) {
return new FileHelper<A>(
path,
@@ -110,13 +110,13 @@ export class FileHelper<A> {
},
(inString) => {
return shape.unsafeCast(JSON.parse(inString));
}
},
);
}
static toml<A extends Record<string, unknown>>(
path: string,
volume: string,
shape: matches.Validator<unknown, A>
shape: matches.Validator<unknown, A>,
) {
return new FileHelper<A>(
path,
@@ -126,13 +126,13 @@ export class FileHelper<A> {
},
(inString) => {
return shape.unsafeCast(TOML.parse(inString));
}
},
);
}
static yaml<A extends Record<string, unknown>>(
path: string,
volume: string,
shape: matches.Validator<unknown, A>
shape: matches.Validator<unknown, A>,
) {
return new FileHelper<A>(
path,
@@ -142,7 +142,7 @@ export class FileHelper<A> {
},
(inString) => {
return shape.unsafeCast(YAML.parse(inString));
}
},
);
}
}

View File

@@ -1,45 +1,53 @@
import { Parser } from "ts-matches";
import { Effects } from "../types";
import { Effects, EnsureWrapperDataPath, ExtractWrapperData } from "../types";
import { NoAny } from ".";
export function getWrapperData<A>(
effects: Effects,
validator: Parser<unknown, A>,
options: {
export class WrapperData<WrapperData, Path extends string> {
constructor(
readonly effects: Effects,
readonly path: Path & EnsureWrapperDataPath<WrapperData, Path>,
readonly options: {
/** Defaults to what ever the package currently in */
packageId?: string | undefined;
/** JsonPath */
path?: string | undefined;
} = {}
) {
return {
const: () =>
effects
.getWrapperData({
...options,
callback: effects.restart,
})
.then(validator.unsafeCast),
first: () =>
effects
.getWrapperData({
...options,
} = {},
) {}
const() {
return this.effects.getWrapperData<WrapperData, Path>({
...this.options,
path: this.path as any,
callback: this.effects.restart,
});
}
first() {
return this.effects.getWrapperData<WrapperData, Path>({
...this.options,
path: this.path as any,
callback: () => {},
})
.then(validator.unsafeCast),
overTime: async function* <A>() {
});
}
async *overTime() {
while (true) {
let callback: () => void;
const waitForNext = new Promise<void>((resolve) => {
callback = resolve;
});
yield await effects
.getWrapperData({
...options,
yield await this.effects.getWrapperData<WrapperData, Path>({
...this.options,
path: this.path as any,
callback: () => callback(),
})
.then(validator.unsafeCast);
});
await waitForNext;
}
},
};
}
}
export function getWrapperData<WrapperData, Path extends string>(
effects: Effects,
path: Path & EnsureWrapperDataPath<WrapperData, Path>,
options: {
/** Defaults to what ever the package currently in */
packageId?: string | undefined;
} = {},
) {
return new WrapperData<WrapperData, Path>(effects, path as any, options);
}

View File

@@ -5,20 +5,23 @@ import nullIfEmpty from "./nullIfEmpty";
import { getWrapperData } from "./getWrapperData";
import { checkPortListening, checkWebUrl } from "../health/checkFns";
import { LocalPort, NetworkBuilder, TorHostname } from "../mainFn";
import { ExtractWrapperData } from "../types";
export { guardAll, typeFromProps } from "./propertiesMatcher";
export { default as nullIfEmpty } from "./nullIfEmpty";
export { FileHelper } from "./fileHelper";
export { getWrapperData } from "./getWrapperData";
export { deepEqual } from "./deepEqual";
export { deepMerge } from "./deepMerge";
/** Used to check if the file exists before hand */
export const exists = (
effects: T.Effects,
props: { path: string; volumeId: string }
props: { path: string; volumeId: string },
) =>
effects.metadata(props).then(
(_) => true,
(_) => false
(_) => false,
);
export const isKnownError = (e: unknown): e is T.KnownError =>
@@ -26,28 +29,40 @@ export const isKnownError = (e: unknown): e is T.KnownError =>
type Cdr<A> = A extends [unknown, ...infer Cdr] ? Cdr : [];
export const utils = (effects: T.Effects) => ({
declare const affine: unique symbol;
function withAffine<B>() {
return {} as { [affine]: B };
}
export const utils = <WrapperData = never>(effects: T.Effects) => ({
readFile: <A>(fileHelper: FileHelper<A>) => fileHelper.read(effects),
writeFile: <A>(fileHelper: FileHelper<A>, data: A) =>
fileHelper.write(data, effects),
exists: (props: { path: string; volumeId: string }) => exists(effects, props),
nullIfEmpty,
getWrapperData: <A>(
validator: Parser<unknown, A>,
getWrapperData: <Path extends string>(
path: T.EnsureWrapperDataPath<WrapperData, Path>,
options: {
validator?: Parser<unknown, ExtractWrapperData<WrapperData, Path>>;
/** Defaults to what ever the package currently in */
packageId?: string | undefined;
/** JsonPath */
path?: string | undefined;
} = {}
) => getWrapperData(effects, validator, options),
setWrapperData: <A>(
value: A,
options: { packageId?: string | undefined; path?: string | undefined } = {}
) => effects.setWrapperData({ ...options, value }),
} = {},
) => getWrapperData<WrapperData, Path>(effects, path as any, options),
setWrapperData: <Path extends string | never>(
path: T.EnsureWrapperDataPath<WrapperData, Path>,
value: ExtractWrapperData<WrapperData, Path>,
) => effects.setWrapperData<WrapperData, Path>({ value, path: path as any }),
checkPortListening: checkPortListening.bind(null, effects),
checkWebUrl: checkWebUrl.bind(null, effects),
localPort: LocalPort.bind(null, effects),
networkBuilder: NetworkBuilder.of.bind(null, effects),
torHostName: TorHostname.of.bind(null, effects),
});
type NeverPossible = { [affine]: string };
export type NoAny<A> = NeverPossible extends A
? keyof NeverPossible extends keyof A
? never
: A
: A;

View File

@@ -141,7 +141,7 @@ function charRange(value = "") {
*/
export function generateDefault(
generate: { charset: string; len: number },
{ random = () => Math.random() } = {}
{ random = () => Math.random() } = {},
) {
const validCharSets: number[][] = generate.charset
.split(",")
@@ -152,7 +152,7 @@ export function generateDefault(
}
const max = validCharSets.reduce(
(acc, x) => x.reduce((x, y) => Math.max(x, y), acc),
0
0,
);
let i = 0;
const answer: string[] = Array(generate.len);
@@ -161,7 +161,7 @@ export function generateDefault(
const inRange = validCharSets.reduce(
(acc, [lower, upper]) =>
acc || (nextValue >= lower && nextValue <= upper),
false
false,
);
if (!inRange) continue;
answer[i] = String.fromCharCode(nextValue);
@@ -186,7 +186,7 @@ export function matchNumberWithRange(range: string) {
? "any"
: left === "["
? `greaterThanOrEqualTo${leftValue}`
: `greaterThan${leftValue}`
: `greaterThan${leftValue}`,
)
.validate(
// prettier-ignore
@@ -196,7 +196,7 @@ export function matchNumberWithRange(range: string) {
// prettier-ignore
rightValue === "*" ? "any" :
right === "]" ? `lessThanOrEqualTo${rightValue}` :
`lessThan${rightValue}`
`lessThan${rightValue}`,
);
}
function withIntegral(parser: Parser<unknown, number>, value: unknown) {
@@ -219,7 +219,7 @@ function defaultRequired<A>(parser: Parser<unknown, A>, value: unknown) {
if (matchDefault.test(value)) {
if (isGenerator(value.default)) {
return parser.defaultTo(
parser.unsafeCast(generateDefault(value.default))
parser.unsafeCast(generateDefault(value.default)),
);
}
return parser.defaultTo(value.default);
@@ -237,7 +237,7 @@ function defaultRequired<A>(parser: Parser<unknown, A>, value: unknown) {
* @returns
*/
export function guardAll<A extends ValueSpecAny>(
value: A
value: A,
): Parser<unknown, GuardAll<A>> {
if (!isType.test(value)) {
return unknown as any;
@@ -255,7 +255,7 @@ export function guardAll<A extends ValueSpecAny>(
case "number":
return defaultRequired(
withIntegral(withRange(value), value),
value
value,
) as any;
case "object":
@@ -274,7 +274,7 @@ export function guardAll<A extends ValueSpecAny>(
matches
.arrayOf(guardAll(spec as any))
.validate((x) => rangeValidate(x.length), "valid length"),
value
value,
) as any;
}
case "select":
@@ -282,7 +282,7 @@ export function guardAll<A extends ValueSpecAny>(
const valueKeys = Object.keys(value.values);
return defaultRequired(
literals(valueKeys[0], ...valueKeys),
value
value,
) as any;
}
return unknown as any;
@@ -290,19 +290,19 @@ export function guardAll<A extends ValueSpecAny>(
case "multiselect":
if (matchValues.test(value)) {
const maybeAddRangeValidate = <X extends Validator<unknown, B[]>, B>(
x: X
x: X,
) => {
if (!matchRange.test(value)) return x;
return x.validate(
(x) => matchNumberWithRange(value.range).test(x.length),
"validLength"
"validLength",
);
};
const valueKeys = Object.keys(value.values);
return defaultRequired(
maybeAddRangeValidate(arrayOf(literals(valueKeys[0], ...valueKeys))),
value
value,
) as any;
}
return unknown as any;
@@ -316,8 +316,8 @@ export function guardAll<A extends ValueSpecAny>(
object({
unionSelectKey: literals(name),
unionValueKey: typeFromProps(spec),
})
)
}),
),
) as any;
}
return unknown as any;
@@ -334,7 +334,7 @@ export function guardAll<A extends ValueSpecAny>(
* @returns
*/
export function typeFromProps<A extends InputSpec>(
valueDictionary: A
valueDictionary: A,
): Parser<unknown, TypeFromProps<A>> {
if (!recordString.test(valueDictionary)) return unknown as any;
return object(
@@ -342,7 +342,7 @@ export function typeFromProps<A extends InputSpec>(
Object.entries(valueDictionary).map(([key, value]) => [
key,
guardAll(value),
])
)
]),
),
) as any;
}

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "start-sdk",
"version": "0.4.0-lib0.charlie30",
"version": "0.4.0-lib0.charlie31",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "start-sdk",
"version": "0.4.0-lib0.charlie30",
"version": "0.4.0-lib0.charlie31",
"license": "MIT",
"dependencies": {
"@iarna/toml": "^2.2.5",

View File

@@ -1,6 +1,6 @@
{
"name": "start-sdk",
"version": "0.4.0-lib0.charlie30",
"version": "0.4.0-lib0.charlie31",
"description": "For making the patterns that are wanted in making services for the startOS.",
"main": "./lib/index.js",
"types": "./lib/index.d.ts",
@@ -26,6 +26,12 @@
"ts-matches": "^5.4.1",
"yaml": "^2.2.1"
},
"prettier": {
"trailingComma": "all",
"tabWidth": 2,
"semi": true,
"singleQuote": false
},
"devDependencies": {
"@types/jest": "^29.4.0",
"@types/lodash": "^4.14.191",

View File

@@ -6,16 +6,16 @@ import { unionSelectKey } from "../lib/config/configTypes";
export async function writeConvertedFile(
file: string,
inputData: Promise<any> | any,
options: Parameters<typeof makeFileContent>[1]
options: Parameters<typeof makeFileContent>[1],
) {
await fs.writeFile(file, await makeFileContent(inputData, options), (err) =>
console.error(err)
console.error(err),
);
}
export default async function makeFileContent(
inputData: Promise<any> | any,
{ startSdk = "start-sdk" } = {}
{ startSdk = "start-sdk" } = {},
) {
const outputLines: string[] = [];
outputLines.push(`
@@ -27,10 +27,10 @@ export default async function makeFileContent(
const configName = newConst("InputSpec", convertInputSpec(data));
const configMatcherName = newConst(
"matchInputSpec",
`${configName}.validator()`
`${configName}.validator()`,
);
outputLines.push(
`export type InputSpec = typeof ${configMatcherName}._TYPE;`
`export type InputSpec = typeof ${configMatcherName}._TYPE;`,
);
return outputLines.join("\n");
@@ -62,7 +62,7 @@ export default async function makeFileContent(
placeholder: value.placeholder || null,
},
null,
2
2,
)})`;
}
return `Value.string(${JSON.stringify(
@@ -78,7 +78,7 @@ export default async function makeFileContent(
patternDescription: value["pattern-description"] || null,
},
null,
2
2,
)})`;
}
case "number": {
@@ -95,7 +95,7 @@ export default async function makeFileContent(
placeholder: value.placeholder || null,
},
null,
2
2,
)})`;
}
case "boolean": {
@@ -107,7 +107,7 @@ export default async function makeFileContent(
warning: value.warning || null,
},
null,
2
2,
)})`;
}
case "enum": {
@@ -118,7 +118,7 @@ export default async function makeFileContent(
const values = Object.fromEntries(
Array.from(allValueNames)
.filter(string.test)
.map((key) => [key, value?.spec?.["value-names"]?.[key] || key])
.map((key) => [key, value?.spec?.["value-names"]?.[key] || key]),
);
return `Value.select(${JSON.stringify(
{
@@ -130,13 +130,13 @@ export default async function makeFileContent(
values,
},
null,
2
2,
)} as const)`;
}
case "object": {
const specName = newConst(
value.name + "_spec",
convertInputSpec(value.spec)
convertInputSpec(value.spec),
);
return `Value.object({
name: ${JSON.stringify(value.name || null)},
@@ -147,7 +147,7 @@ export default async function makeFileContent(
case "union": {
const variants = newConst(
value.name + "_variants",
convertVariants(value.variants, value.tag["variant-names"] || {})
convertVariants(value.variants, value.tag["variant-names"] || {}),
);
return `Value.union({
@@ -183,7 +183,7 @@ export default async function makeFileContent(
warning: value.warning || null,
},
null,
2
2,
)}, ${JSON.stringify({
masked: value?.spec?.masked || false,
placeholder: value?.spec?.placeholder || null,
@@ -201,7 +201,7 @@ export default async function makeFileContent(
warning: value.warning || null,
},
null,
2
2,
)}, ${JSON.stringify({
range: value?.spec?.range || null,
integral: value?.spec?.integral || false,
@@ -212,7 +212,7 @@ export default async function makeFileContent(
case "enum": {
const allValueNames = new Set(
...(value?.spec?.["values"] || []),
...Object.keys(value?.spec?.["value-names"] || {})
...Object.keys(value?.spec?.["value-names"] || {}),
);
const values = Object.fromEntries(
Array.from(allValueNames)
@@ -220,7 +220,7 @@ export default async function makeFileContent(
.map((key: string) => [
key,
value?.spec?.["value-names"]?.[key] || key,
])
]),
);
return `Value.multiselect(${JSON.stringify(
{
@@ -232,13 +232,13 @@ export default async function makeFileContent(
values,
},
null,
2
2,
)})`;
}
case "object": {
const specName = newConst(
value.name + "_spec",
convertInputSpec(value.spec.spec)
convertInputSpec(value.spec.spec),
);
return `List.obj({
name: ${JSON.stringify(value.name || null)},
@@ -257,8 +257,8 @@ export default async function makeFileContent(
value.name + "_variants",
convertVariants(
value.spec.variants,
value.spec["variant-names"] || {}
)
value.spec["variant-names"] || {},
),
);
const unionValueName = newConst(
value.name + "_union",
@@ -266,13 +266,13 @@ export default async function makeFileContent(
Value.union({
name: ${JSON.stringify(value?.spec?.tag?.name || null)},
description: ${JSON.stringify(
value?.spec?.tag?.description || null
value?.spec?.tag?.description || null,
)},
warning: ${JSON.stringify(value?.spec?.tag?.warning || null)},
required: ${JSON.stringify(!(value?.spec?.tag?.nullable || false))},
default: ${JSON.stringify(value?.spec?.default || null)},
}, ${variants})
`
`,
);
const listConfig = newConst(
value.name + "_list_config",
@@ -280,7 +280,7 @@ export default async function makeFileContent(
Config.of({
"union": ${unionValueName}
})
`
`,
);
return `List.obj({
name:${JSON.stringify(value.name || null)},
@@ -300,7 +300,7 @@ export default async function makeFileContent(
function convertVariants(
variants: Record<string, unknown>,
variantNames: Record<string, string>
variantNames: Record<string, string>,
): string {
let answer = "Variants.of({";
for (const [key, value] of Object.entries(variants)) {