From 69e23a36b5ac38bd28c9dfeb345031f957405572 Mon Sep 17 00:00:00 2001 From: Kevin Ingersoll Date: Thu, 18 Jul 2024 18:56:20 +0100 Subject: [PATCH] feat(store,world): add `namespaces` to config output (#2958) --- packages/store/ts/config/v2/input.ts | 18 +- packages/store/ts/config/v2/namespace.ts | 61 +++++ packages/store/ts/config/v2/output.ts | 24 +- packages/store/ts/config/v2/store.test.ts | 253 ++++++++++++------ packages/store/ts/config/v2/store.ts | 57 ++-- .../ts/config/v2/storeWithShorthands.test.ts | 96 +++++-- packages/store/ts/config/v2/table.test.ts | 6 +- packages/store/ts/config/v2/table.ts | 2 +- packages/world/ts/config/v2/world.test.ts | 168 ++++++++---- packages/world/ts/config/v2/world.ts | 5 +- .../ts/config/v2/worldWithShorthands.test.ts | 103 +++++-- .../world/ts/config/v2/worldWithShorthands.ts | 41 +-- 12 files changed, 574 insertions(+), 260 deletions(-) create mode 100644 packages/store/ts/config/v2/namespace.ts diff --git a/packages/store/ts/config/v2/input.ts b/packages/store/ts/config/v2/input.ts index 51814c5191..7a721c9503 100644 --- a/packages/store/ts/config/v2/input.ts +++ b/packages/store/ts/config/v2/input.ts @@ -52,7 +52,21 @@ export type TablesInput = { export type CodegenInput = Partial; -export type StoreInput = { +export type NamespaceInput = { + /** + * Human-readable namespace label. Used as config keys and directory names. + * Labels are not length constrained like namespaces within resource IDs, but special characters should be avoided to be compatible with the filesystem, Solidity compiler, etc. + */ + readonly label: string; + /** + * Namespace used in resource ID. + * Defaults to the first 16 characters of `label` if not set. + */ + readonly namespace?: string; + readonly tables?: TablesInput; +}; + +export type StoreInput = Omit & { /** * Directory of Solidity source relative to the MUD config. * This is used to resolve other paths in the config, like codegen and user types. @@ -60,8 +74,6 @@ export type StoreInput = { * Defaults to `src` to match `foundry.toml`'s default. If you change this from the default, you may also need to configure foundry with the same source directory. */ readonly sourceDirectory?: string; - readonly namespace?: string; - readonly tables?: TablesInput; readonly userTypes?: UserTypes; readonly enums?: EnumsInput; readonly codegen?: CodegenInput; diff --git a/packages/store/ts/config/v2/namespace.ts b/packages/store/ts/config/v2/namespace.ts new file mode 100644 index 0000000000..44d4583ec8 --- /dev/null +++ b/packages/store/ts/config/v2/namespace.ts @@ -0,0 +1,61 @@ +import { ErrorMessage, flatMorph } from "@arktype/util"; +import { hasOwnKey, mergeIfUndefined } from "./generics"; +import { NamespaceInput } from "./input"; +import { resolveTables, validateTables } from "./tables"; +import { AbiTypeScope, Scope } from "./scope"; + +export type validateNamespace = { + [key in keyof input]: key extends "tables" + ? validateTables + : key extends keyof NamespaceInput + ? NamespaceInput[key] + : ErrorMessage<`\`${key & string}\` is not a valid namespace config option.`>; +}; + +export function validateNamespace( + input: unknown, + scope: scope, +): asserts input is NamespaceInput { + if (hasOwnKey(input, "namespace") && typeof input.namespace === "string" && input.namespace.length > 14) { + throw new Error(`\`namespace\` must fit into a \`bytes14\`, but "${input.namespace}" is too long.`); + } + if (hasOwnKey(input, "tables")) { + validateTables(input.tables, scope); + } +} + +export type resolveNamespace = input extends NamespaceInput + ? { + readonly label: input["label"]; + readonly namespace: string; + readonly tables: undefined extends input["tables"] + ? {} + : resolveTables< + { + readonly [label in keyof input["tables"]]: mergeIfUndefined< + input["tables"][label], + { readonly namespace: string } + >; + }, + scope + >; + } + : never; + +export function resolveNamespace( + input: input, + scope: scope, +): resolveNamespace { + const label = input.label; + const namespace = input.namespace ?? label.slice(0, 14); + return { + label, + namespace, + tables: resolveTables( + flatMorph(input.tables ?? {}, (label, table) => { + return [label, mergeIfUndefined(table, { namespace })]; + }), + scope, + ), + } as never; +} diff --git a/packages/store/ts/config/v2/output.ts b/packages/store/ts/config/v2/output.ts index e3f3c816e7..4d10e71a71 100644 --- a/packages/store/ts/config/v2/output.ts +++ b/packages/store/ts/config/v2/output.ts @@ -66,7 +66,24 @@ export type Codegen = { readonly indexFilename: string; }; -export type Store = { +export type Namespace = { + /** + * Human-readable namespace label. Used as config keys and directory names. + * Labels are not length constrained like namespaces within resource IDs, but special characters should be avoided to be compatible with the filesystem, Solidity compiler, etc. + */ + readonly label: string; + /** + * Namespace used in resource ID. + */ + readonly namespace: string; + readonly tables: Tables; +}; + +export type Namespaces = { + readonly [label: string]: Namespace; +}; + +export type Store = Omit & { /** * Directory of Solidity source relative to the MUD config. * This is used to resolve other paths in the config, like codegen and user types. @@ -74,12 +91,9 @@ export type Store = { * Defaults to `src` to match `foundry.toml`'s default. If you change this from the default, you may also need to configure foundry with the same source directory. */ readonly sourceDirectory: string; - readonly tables: { - readonly [label: string]: Table; - }; readonly userTypes: UserTypes; readonly enums: EnumsInput; readonly enumValues: EnumValues; - readonly namespace: string; readonly codegen: Codegen; + readonly namespaces: Namespaces; }; diff --git a/packages/store/ts/config/v2/store.test.ts b/packages/store/ts/config/v2/store.test.ts index 018c2769f7..44c0f1037a 100644 --- a/packages/store/ts/config/v2/store.test.ts +++ b/packages/store/ts/config/v2/store.test.ts @@ -4,6 +4,7 @@ import { attest } from "@arktype/attest"; import { resourceToHex } from "@latticexyz/common"; import { CODEGEN_DEFAULTS, TABLE_CODEGEN_DEFAULTS, TABLE_DEPLOY_DEFAULTS } from "./defaults"; import { Store } from "./output"; +import { satisfy } from "@arktype/util"; describe("defineStore", () => { it("should return the full config given a full config with one key", () => { @@ -16,13 +17,13 @@ describe("defineStore", () => { }, }); - const expected = { - sourceDirectory: "src", + const expectedBaseNamespace = { + namespace: "" as string, tables: { Example: { label: "Example", type: "table", - namespace: "", + namespace: "" as string, name: "Example" as string, tableId: resourceToHex({ type: "table", namespace: "", name: "Example" }), schema: { @@ -44,14 +45,24 @@ describe("defineStore", () => { deploy: TABLE_DEPLOY_DEFAULTS, }, }, + } as const; + + const expectedConfig = { + ...expectedBaseNamespace, + namespaces: { + "": { + label: "", + ...expectedBaseNamespace, + }, + }, + sourceDirectory: "src", userTypes: {}, enums: {}, enumValues: {}, - namespace: "", codegen: CODEGEN_DEFAULTS, } as const; - attest(config).equals(expected); + attest(config).equals(expectedConfig); }); it("should return the full config given a full config with one key and user types", () => { @@ -68,13 +79,13 @@ describe("defineStore", () => { }, }); - const expected = { - sourceDirectory: "src", + const expectedBaseNamespace = { + namespace: "" as string, tables: { Example: { label: "Example", type: "table", - namespace: "", + namespace: "" as string, name: "Example" as string, tableId: resourceToHex({ type: "table", namespace: "", name: "Example" }), schema: { @@ -96,17 +107,27 @@ describe("defineStore", () => { deploy: TABLE_DEPLOY_DEFAULTS, }, }, + } as const; + + const expectedConfig = { + ...expectedBaseNamespace, + namespaces: { + "": { + label: "", + ...expectedBaseNamespace, + }, + }, + sourceDirectory: "src", userTypes: { static: { type: "address", filePath: "path/to/file" }, dynamic: { type: "string", filePath: "path/to/file" }, }, enums: {}, enumValues: {}, - namespace: "", codegen: CODEGEN_DEFAULTS, } as const; - attest(config).equals(expected); + attest(config).equals(expectedConfig); }); it("should return the full config given a full config with two key", () => { @@ -119,13 +140,13 @@ describe("defineStore", () => { }, }); - const expected = { - sourceDirectory: "src", + const expectedBaseNamespace = { + namespace: "" as string, tables: { Example: { label: "Example", type: "table", - namespace: "", + namespace: "" as string, name: "Example" as string, tableId: resourceToHex({ type: "table", namespace: "", name: "Example" }), schema: { @@ -147,14 +168,24 @@ describe("defineStore", () => { deploy: TABLE_DEPLOY_DEFAULTS, }, }, + } as const; + + const expectedConfig = { + ...expectedBaseNamespace, + namespaces: { + "": { + label: "", + ...expectedBaseNamespace, + }, + }, + sourceDirectory: "src", userTypes: {}, enums: {}, enumValues: {}, - namespace: "", codegen: CODEGEN_DEFAULTS, } as const; - attest(config).equals(expected); + attest(config).equals(expectedConfig); }); it("should resolve two tables in the config with different schemas", () => { @@ -171,13 +202,13 @@ describe("defineStore", () => { }, }); - const expected = { - sourceDirectory: "src", + const expectedBaseNamespace = { + namespace: "" as string, tables: { First: { label: "First", type: "table", - namespace: "", + namespace: "" as string, name: "First" as string, tableId: resourceToHex({ type: "table", namespace: "", name: "First" }), schema: { @@ -201,7 +232,7 @@ describe("defineStore", () => { Second: { label: "Second", type: "table", - namespace: "", + namespace: "" as string, name: "Second" as string, tableId: resourceToHex({ type: "table", namespace: "", name: "Second" }), schema: { @@ -223,14 +254,24 @@ describe("defineStore", () => { deploy: TABLE_DEPLOY_DEFAULTS, }, }, + } as const; + + const expectedConfig = { + ...expectedBaseNamespace, + namespaces: { + "": { + label: "", + ...expectedBaseNamespace, + }, + }, + sourceDirectory: "src", userTypes: {}, enums: {}, enumValues: {}, - namespace: "", codegen: CODEGEN_DEFAULTS, } as const; - attest(config).equals(expected); + attest(config).equals(expectedConfig); }); it("should resolve two tables in the config with different schemas and user types", () => { @@ -251,13 +292,13 @@ describe("defineStore", () => { }, }); - const expected = { - sourceDirectory: "src", + const expectedBaseNamespace = { + namespace: "" as string, tables: { First: { label: "First", type: "table", - namespace: "", + namespace: "" as string, name: "First" as string, tableId: resourceToHex({ type: "table", namespace: "", name: "First" }), schema: { @@ -281,7 +322,7 @@ describe("defineStore", () => { Second: { label: "Second", type: "table", - namespace: "", + namespace: "" as string, name: "Second" as string, tableId: resourceToHex({ type: "table", namespace: "", name: "Second" }), schema: { @@ -303,17 +344,27 @@ describe("defineStore", () => { deploy: TABLE_DEPLOY_DEFAULTS, }, }, + } as const; + + const expectedConfig = { + ...expectedBaseNamespace, + namespaces: { + "": { + label: "", + ...expectedBaseNamespace, + }, + }, + sourceDirectory: "src", userTypes: { Static: { type: "address", filePath: "path/to/file" }, Dynamic: { type: "string", filePath: "path/to/file" }, }, enums: {}, enumValues: {}, - namespace: "", codegen: CODEGEN_DEFAULTS, } as const; - attest(config).equals(expected); + attest(config).equals(expectedConfig); }); it("should throw if referring to fields of different tables", () => { @@ -387,13 +438,14 @@ describe("defineStore", () => { ValidNames: ["first", "second"], }, }); - const expected = { - sourceDirectory: "src", + + const expectedBaseNamespace = { + namespace: "" as string, tables: { Example: { label: "Example", type: "table", - namespace: "", + namespace: "" as string, name: "Example" as string, tableId: resourceToHex({ type: "table", namespace: "", name: "Example" }), schema: { @@ -415,6 +467,17 @@ describe("defineStore", () => { deploy: TABLE_DEPLOY_DEFAULTS, }, }, + } as const; + + const expectedConfig = { + ...expectedBaseNamespace, + namespaces: { + "": { + label: "", + ...expectedBaseNamespace, + }, + }, + sourceDirectory: "src", userTypes: { static: { type: "address", filePath: "path/to/file" }, dynamic: { type: "string", filePath: "path/to/file" }, @@ -428,32 +491,29 @@ describe("defineStore", () => { second: 1, }, }, - namespace: "", codegen: CODEGEN_DEFAULTS, } as const; - attest(config).equals(expected); + attest(config).equals(expectedConfig); }); it("should use the root namespace as default namespace", () => { const config = defineStore({}); - - attest<"">(config.namespace).equals(""); + attest(config.namespace).equals(""); }); - it("should use pipe through non-default namespaces", () => { + it("should use pipe through non-default namespace", () => { const config = defineStore({ namespace: "custom" }); - - attest<"custom">(config.namespace).equals("custom"); + attest(config.namespace).equals("custom"); }); - it("should extend the output Config type", () => { + it("should satisfy the output type", () => { const config = defineStore({ tables: { Name: { schema: { id: "address" }, key: ["id"] } }, userTypes: { CustomType: { type: "address", filePath: "path/to/file" } }, }); - attest(); + attest>(); }); it("should use the global namespace instead for tables", () => { @@ -467,10 +527,10 @@ describe("defineStore", () => { }, }); - attest<"namespace">(config.namespace).equals("namespace"); + attest(config.namespace).equals("namespace"); attest<"Example">(config.tables.namespace__Example.label).equals("Example"); - attest<"namespace">(config.tables.namespace__Example.namespace).equals("namespace"); - attest(config.tables.namespace__Example.name).equals("Example"); + attest(config.tables.namespace__Example.namespace).equals("namespace"); + attest(config.tables.namespace__Example.name).equals("Example"); attest(config.tables.namespace__Example.tableId).equals( resourceToHex({ type: "table", name: "Example", namespace: "namespace" }), ); @@ -582,54 +642,66 @@ describe("defineStore", () => { }, }); - const expected = { - sourceDirectory: "src", - tables: { - app__NamespaceDir: { - label: "NamespaceDir", - type: "table", - namespace: "app", - name: "NamespaceDir" as string, - tableId: resourceToHex({ type: "table", namespace: "app", name: "NamespaceDir" }), - schema: { - name: { - type: "string", - internalType: "string", - }, + const expectedTables = { + NamespaceDir: { + label: "NamespaceDir", + type: "table", + namespace: "app" as string, + name: "NamespaceDir" as string, + tableId: resourceToHex({ type: "table", namespace: "app", name: "NamespaceDir" }), + schema: { + name: { + type: "string", + internalType: "string", }, - key: [], - codegen: { - ...TABLE_CODEGEN_DEFAULTS, - dataStruct: false as boolean, - outputDirectory: "app/tables" as string, - }, - deploy: TABLE_DEPLOY_DEFAULTS, }, - app__NotNamespaceDir: { - label: "NotNamespaceDir", - type: "table", - namespace: "app", - name: "NotNamespaceDir" as string, - tableId: resourceToHex({ type: "table", namespace: "app", name: "NotNamespaceDir" }), - schema: { - name: { - type: "string", - internalType: "string", - }, - }, - key: [], - codegen: { - ...TABLE_CODEGEN_DEFAULTS, - dataStruct: false as boolean, - outputDirectory: "tables", + key: [], + codegen: { + ...TABLE_CODEGEN_DEFAULTS, + dataStruct: false as boolean, + outputDirectory: "app/tables" as string, + }, + deploy: TABLE_DEPLOY_DEFAULTS, + }, + NotNamespaceDir: { + label: "NotNamespaceDir", + type: "table", + namespace: "app" as string, + name: "NotNamespaceDir" as string, + tableId: resourceToHex({ type: "table", namespace: "app", name: "NotNamespaceDir" }), + schema: { + name: { + type: "string", + internalType: "string", }, - deploy: TABLE_DEPLOY_DEFAULTS, + }, + key: [], + codegen: { + ...TABLE_CODEGEN_DEFAULTS, + dataStruct: false as boolean, + outputDirectory: "tables", + }, + deploy: TABLE_DEPLOY_DEFAULTS, + }, + } as const; + + const expectedConfig = { + namespace: "app" as string, + tables: { + app__NamespaceDir: expectedTables.NamespaceDir, + app__NotNamespaceDir: expectedTables.NotNamespaceDir, + }, + namespaces: { + app: { + label: "app", + namespace: "app" as string, + tables: expectedTables, }, }, + sourceDirectory: "src", userTypes: {}, enums: {}, enumValues: {}, - namespace: "app", codegen: { ...CODEGEN_DEFAULTS, namespaceDirectories: true, @@ -637,14 +709,17 @@ describe("defineStore", () => { } as const; // Running attest on the whole object is hard to parse when it fails, so test the inner objects first - attest(config.codegen).equals(expected.codegen); - attest(config.tables.app__NamespaceDir.codegen).equals( - expected.tables.app__NamespaceDir.codegen, - ); - attest(config.tables.app__NotNamespaceDir.codegen).equals( - expected.tables.app__NotNamespaceDir.codegen, + attest(config.codegen).equals(expectedConfig.codegen); + attest(config.tables.app__NamespaceDir.codegen).equals( + expectedConfig.tables.app__NamespaceDir.codegen, ); + attest( + config.tables.app__NotNamespaceDir.codegen, + ).equals(expectedConfig.tables.app__NotNamespaceDir.codegen); + + attest(config.tables.app__NamespaceDir).equals(config.namespaces.app.tables.NamespaceDir); + attest(config.tables.app__NotNamespaceDir).equals(config.namespaces.app.tables.NotNamespaceDir); - attest(config).equals(expected); + attest(config).equals(expectedConfig); }); }); diff --git a/packages/store/ts/config/v2/store.ts b/packages/store/ts/config/v2/store.ts index 34e040d10f..3c632c9ac4 100644 --- a/packages/store/ts/config/v2/store.ts +++ b/packages/store/ts/config/v2/store.ts @@ -9,6 +9,7 @@ import { mapEnums, resolveEnums, scopeWithEnums } from "./enums"; import { resolveCodegen } from "./codegen"; import { resolveNamespacedTables } from "./namespacedTables"; import { resolveTable } from "./table"; +import { resolveNamespace } from "./namespace"; export type extendedScope = scopeWithEnums, scopeWithUserTypes>>; @@ -30,6 +31,11 @@ export type validateStore = { export function validateStore(input: unknown): asserts input is StoreInput { const scope = extendedScope(input); + + if (hasOwnKey(input, "namespace") && typeof input.namespace === "string" && input.namespace.length > 14) { + throw new Error(`\`namespace\` must fit into a \`bytes14\`, but "${input.namespace}" is too long.`); + } + if (hasOwnKey(input, "tables")) { validateTables(input.tables, scope); } @@ -47,7 +53,7 @@ export type resolveStore< : CONFIG_DEFAULTS["namespace"] : CONFIG_DEFAULTS["namespace"], > = { - readonly namespace: namespace; + readonly namespace: string; readonly sourceDirectory: "sourceDirectory" extends keyof input ? input["sourceDirectory"] : CONFIG_DEFAULTS["sourceDirectory"]; @@ -66,6 +72,15 @@ export type resolveStore< readonly enums: "enums" extends keyof input ? show> : {}; readonly enumValues: "enums" extends keyof input ? show> : {}; readonly codegen: "codegen" extends keyof input ? resolveCodegen : resolveCodegen<{}>; + readonly namespaces: { + readonly [label in namespace & string]: resolveNamespace< + { + readonly label: label; + readonly tables: "tables" extends keyof input ? input["tables"] : undefined; + }, + extendedScope + >; + }; }; export function resolveStore(input: input): resolveStore { @@ -73,30 +88,34 @@ export function resolveStore(input: input): reso const namespace = input.namespace ?? CONFIG_DEFAULTS["namespace"]; const codegen = resolveCodegen(input.codegen); - const tables = resolveTables( - flatMorph(input.tables ?? {}, (label, table) => { - return [ + const tablesInput = flatMorph(input.tables ?? {}, (label, table) => { + return [ + label, + { + ...table, label, - { - ...table, - label, - namespace, - codegen: { - ...table.codegen, - outputDirectory: - table.codegen?.outputDirectory ?? - (codegen.namespaceDirectories && namespace !== "" ? `${namespace}/tables` : "tables"), - }, + namespace, + codegen: { + ...table.codegen, + outputDirectory: + table.codegen?.outputDirectory ?? + (codegen.namespaceDirectories && namespace !== "" ? `${namespace}/tables` : "tables"), }, - ]; - }), - scope, - ); + }, + ]; + }); + + const namespaces = { + [namespace]: resolveNamespace({ label: namespace, tables: tablesInput }, scope), + }; + + const tables = resolveTables(tablesInput, scope); return { namespace, - sourceDirectory: input.sourceDirectory ?? CONFIG_DEFAULTS["sourceDirectory"], tables: resolveNamespacedTables(tables, namespace), + namespaces, + sourceDirectory: input.sourceDirectory ?? CONFIG_DEFAULTS["sourceDirectory"], userTypes: input.userTypes ?? {}, enums: resolveEnums(input.enums ?? {}), enumValues: mapEnums(input.enums ?? {}), diff --git a/packages/store/ts/config/v2/storeWithShorthands.test.ts b/packages/store/ts/config/v2/storeWithShorthands.test.ts index e7f3d2d7e8..ba266970c3 100644 --- a/packages/store/ts/config/v2/storeWithShorthands.test.ts +++ b/packages/store/ts/config/v2/storeWithShorthands.test.ts @@ -4,17 +4,20 @@ import { attest } from "@arktype/attest"; import { resourceToHex } from "@latticexyz/common"; import { CODEGEN_DEFAULTS, TABLE_CODEGEN_DEFAULTS, TABLE_DEPLOY_DEFAULTS } from "./defaults"; import { defineStore } from "./store"; +import { satisfy } from "@arktype/util"; +import { Store } from "./output"; describe("defineStoreWithShorthands", () => { it("should accept a shorthand store config as input and expand it", () => { const config = defineStoreWithShorthands({ tables: { Name: "address" } }); - const expected = { - sourceDirectory: "src", + + const expectedBaseNamespace = { + namespace: "" as string, tables: { Name: { label: "Name", type: "table", - namespace: "", + namespace: "" as string, name: "Name" as string, tableId: resourceToHex({ type: "table", namespace: "", name: "Name" }), schema: { @@ -32,14 +35,33 @@ describe("defineStoreWithShorthands", () => { deploy: TABLE_DEPLOY_DEFAULTS, }, }, + } as const; + + const expectedConfig = { + ...expectedBaseNamespace, + namespaces: { + "": { + label: "", + ...expectedBaseNamespace, + }, + }, + sourceDirectory: "src", userTypes: {}, enums: {}, enumValues: {}, - namespace: "", codegen: CODEGEN_DEFAULTS, } as const; - attest(config).equals(expected); + attest(config).equals(expectedConfig); + }); + + it("should satisfy the output type", () => { + const config = defineStore({ + tables: { Name: { schema: { id: "address" }, key: ["id"] } }, + userTypes: { CustomType: { type: "address", filePath: "path/to/file" } }, + }); + + attest>(); }); it("should accept a user type as input and expand it", () => { @@ -47,13 +69,14 @@ describe("defineStoreWithShorthands", () => { tables: { Name: "CustomType" }, userTypes: { CustomType: { type: "address", filePath: "path/to/file" } }, }); - const expected = { - sourceDirectory: "src", + + const expectedBaseNamespace = { + namespace: "" as string, tables: { Name: { label: "Name", type: "table", - namespace: "", + namespace: "" as string, name: "Name" as string, tableId: resourceToHex({ type: "table", namespace: "", name: "Name" }), schema: { @@ -71,28 +94,39 @@ describe("defineStoreWithShorthands", () => { deploy: TABLE_DEPLOY_DEFAULTS, }, }, + } as const; + + const expectedConfig = { + ...expectedBaseNamespace, + namespaces: { + "": { + label: "", + ...expectedBaseNamespace, + }, + }, + sourceDirectory: "src", userTypes: { CustomType: { type: "address", filePath: "path/to/file" } }, enums: {}, enumValues: {}, - namespace: "", codegen: CODEGEN_DEFAULTS, } as const; - attest(config).equals(expected); - attest(expected); + attest(config).equals(expectedConfig); + attest(expectedConfig); }); it("given a schema with a key field with static ABI type, it should use `id` as single key", () => { const config = defineStoreWithShorthands({ tables: { Example: { id: "address", name: "string", age: "uint256" } }, }); - const expected = { - sourceDirectory: "src", + + const expectedBaseNamespace = { + namespace: "" as string, tables: { Example: { label: "Example", type: "table", - namespace: "", + namespace: "" as string, name: "Example" as string, tableId: resourceToHex({ type: "table", namespace: "", name: "Example" }), schema: { @@ -114,14 +148,24 @@ describe("defineStoreWithShorthands", () => { deploy: TABLE_DEPLOY_DEFAULTS, }, }, + } as const; + + const expectedConfig = { + ...expectedBaseNamespace, + namespaces: { + "": { + label: "", + ...expectedBaseNamespace, + }, + }, + sourceDirectory: "src", userTypes: {}, enums: {}, enumValues: {}, - namespace: "", codegen: CODEGEN_DEFAULTS, } as const; - attest(config).equals(expected); + attest(config).equals(expectedConfig); }); it("given a schema with a key field with static custom type, it should use `id` as single key", () => { @@ -129,13 +173,13 @@ describe("defineStoreWithShorthands", () => { tables: { Example: { id: "address", name: "string", age: "uint256" } }, }); - const expected = { - sourceDirectory: "src", + const expectedBaseNamespace = { + namespace: "" as string, tables: { Example: { label: "Example", type: "table", - namespace: "", + namespace: "" as string, name: "Example" as string, tableId: resourceToHex({ type: "table", namespace: "", name: "Example" }), schema: { @@ -157,14 +201,24 @@ describe("defineStoreWithShorthands", () => { deploy: TABLE_DEPLOY_DEFAULTS, }, }, + } as const; + + const expectedConfig = { + ...expectedBaseNamespace, + namespaces: { + "": { + label: "", + ...expectedBaseNamespace, + }, + }, + sourceDirectory: "src", userTypes: {}, enums: {}, enumValues: {}, - namespace: "", codegen: CODEGEN_DEFAULTS, } as const; - attest(config).equals(expected); + attest(config).equals(expectedConfig); }); it("should pass through full table config inputs", () => { diff --git a/packages/store/ts/config/v2/table.test.ts b/packages/store/ts/config/v2/table.test.ts index 193ec4321f..40195d6276 100644 --- a/packages/store/ts/config/v2/table.test.ts +++ b/packages/store/ts/config/v2/table.test.ts @@ -56,7 +56,7 @@ describe("resolveTable", () => { const expected = { label: "", type: "table", - namespace: "", + namespace: "" as string, name: "" as string, tableId: resourceToHex({ type: "table", namespace: "", name: "" }), schema: { @@ -81,7 +81,7 @@ describe("resolveTable", () => { const expected = { label: "", type: "table", - namespace: "", + namespace: "" as string, name: "" as string, tableId: resourceToHex({ type: "table", namespace: "", name: "" }), schema: { @@ -112,7 +112,7 @@ describe("resolveTable", () => { const expected = { label: "", type: "table", - namespace: "", + namespace: "" as string, name: "" as string, tableId: resourceToHex({ type: "table", namespace: "", name: "" }), schema: { diff --git a/packages/store/ts/config/v2/table.ts b/packages/store/ts/config/v2/table.ts index a21ca39297..e711d254a0 100644 --- a/packages/store/ts/config/v2/table.ts +++ b/packages/store/ts/config/v2/table.ts @@ -140,7 +140,7 @@ export type resolveTable = input extends Tab ? { readonly label: input["label"]; readonly type: undefined extends input["type"] ? typeof TABLE_DEFAULTS.type : input["type"]; - readonly namespace: undefined extends input["namespace"] ? typeof TABLE_DEFAULTS.namespace : input["namespace"]; + readonly namespace: string; readonly name: string; readonly tableId: Hex; readonly schema: resolveSchema; diff --git a/packages/world/ts/config/v2/world.test.ts b/packages/world/ts/config/v2/world.test.ts index 7ebb0e9112..695a98790d 100644 --- a/packages/world/ts/config/v2/world.test.ts +++ b/packages/world/ts/config/v2/world.test.ts @@ -40,12 +40,12 @@ describe("defineWorld", () => { const expected = { ...CONFIG_DEFAULTS, - codegen: CODEGEN_DEFAULTS, + namespace: "" as string, tables: { ExampleNS__ExampleTable: { label: "ExampleTable", type: "table", - namespace: "ExampleNS", + namespace: "ExampleNS" as string, name: "ExampleTable" as string, tableId: resourceToHex({ type: "table", namespace: "ExampleNS", name: "ExampleTable" }), schema: { @@ -70,7 +70,7 @@ describe("defineWorld", () => { userTypes: {}, enums: {}, enumValues: {}, - namespace: "", + codegen: CODEGEN_DEFAULTS, } as const; attest(config).equals(expected); @@ -109,7 +109,7 @@ describe("defineWorld", () => { ExampleNS__ExampleTable: { label: "ExampleTable", type: "table", - namespace: "ExampleNS", + namespace: "ExampleNS" as string, name: "ExampleTable" as string, tableId: resourceToHex({ type: "table", namespace: "ExampleNS", name: "ExampleTable" }), schema: { @@ -144,7 +144,7 @@ describe("defineWorld", () => { Second: 1, }, }, - namespace: "", + namespace: "" as string, } as const; attest(config).equals(expected); @@ -195,9 +195,9 @@ describe("defineWorld", () => { }, }); - attest<"namespace">(config.namespace).equals("namespace"); + attest(config.namespace).equals("namespace"); // @ts-expect-error TODO: fix once namespaces support ships - attest<"AnotherOne">(config.tables.AnotherOne__Example.namespace).equals("AnotherOne"); + attest(config.tables.AnotherOne__Example.namespace).equals("AnotherOne"); // @ts-expect-error TODO: fix once namespaces support ships attest(config.tables.AnotherOne__Example.tableId).equals( resourceToHex({ type: "table", name: "Example", namespace: "AnotherOne" }), @@ -215,14 +215,13 @@ describe("defineWorld", () => { }, }); - const expected = { - ...CONFIG_DEFAULTS, - codegen: CODEGEN_DEFAULTS, + const expectedBaseNamespace = { + namespace: "" as string, tables: { Example: { label: "Example", type: "table", - namespace: "", + namespace: "" as string, name: "Example" as string, tableId: resourceToHex({ type: "table", namespace: "", name: "Example" }), schema: { @@ -244,13 +243,24 @@ describe("defineWorld", () => { deploy: TABLE_DEPLOY_DEFAULTS, }, }, + } as const; + + const expectedConfig = { + ...CONFIG_DEFAULTS, + ...expectedBaseNamespace, + namespaces: { + "": { + label: "", + ...expectedBaseNamespace, + }, + }, userTypes: {}, enums: {}, enumValues: {}, - namespace: "", + codegen: CODEGEN_DEFAULTS, } as const; - attest(config).equals(expected); + attest(config).equals(expectedConfig); }); it("should return the full config given a full config with one key and user types", () => { @@ -266,14 +276,14 @@ describe("defineWorld", () => { dynamic: { type: "string", filePath: "path/to/file" }, }, }); - const expected = { - ...CONFIG_DEFAULTS, - codegen: CODEGEN_DEFAULTS, + + const expectedBaseNamespace = { + namespace: "" as string, tables: { Example: { label: "Example", type: "table", - namespace: "", + namespace: "" as string, name: "Example" as string, tableId: resourceToHex({ type: "table", namespace: "", name: "Example" }), schema: { @@ -295,16 +305,27 @@ describe("defineWorld", () => { deploy: TABLE_DEPLOY_DEFAULTS, }, }, + } as const; + + const expectedConfig = { + ...CONFIG_DEFAULTS, + ...expectedBaseNamespace, + namespaces: { + "": { + label: "", + ...expectedBaseNamespace, + }, + }, userTypes: { static: { type: "address", filePath: "path/to/file" }, dynamic: { type: "string", filePath: "path/to/file" }, }, enums: {}, enumValues: {}, - namespace: "", + codegen: CODEGEN_DEFAULTS, } as const; - attest(config).equals(expected); + attest(config).equals(expectedConfig); }); it("should return the full config given a full config with two key", () => { @@ -316,14 +337,14 @@ describe("defineWorld", () => { }, }, }); - const expected = { - ...CONFIG_DEFAULTS, - codegen: CODEGEN_DEFAULTS, + + const expectedBaseNamespace = { + namespace: "" as string, tables: { Example: { label: "Example", type: "table", - namespace: "", + namespace: "" as string, name: "Example" as string, tableId: resourceToHex({ type: "table", namespace: "", name: "Example" }), schema: { @@ -345,14 +366,25 @@ describe("defineWorld", () => { deploy: TABLE_DEPLOY_DEFAULTS, }, }, + } as const; + + const expectedConfig = { + ...CONFIG_DEFAULTS, + ...expectedBaseNamespace, + namespaces: { + "": { + label: "", + ...expectedBaseNamespace, + }, + }, userTypes: {}, enums: {}, enumValues: {}, - namespace: "", + codegen: CODEGEN_DEFAULTS, deploy: DEPLOY_DEFAULTS, } as const; - attest(config).equals(expected); + attest(config).equals(expectedConfig); }); it("should resolve two tables in the config with different schemas", () => { @@ -368,14 +400,14 @@ describe("defineWorld", () => { }, }, }); - const expected = { - ...CONFIG_DEFAULTS, - codegen: CODEGEN_DEFAULTS, + + const expectedBaseNamespace = { + namespace: "" as string, tables: { First: { label: "First", type: "table", - namespace: "", + namespace: "" as string, name: "First" as string, tableId: resourceToHex({ type: "table", namespace: "", name: "First" }), schema: { @@ -399,7 +431,7 @@ describe("defineWorld", () => { Second: { label: "Second", type: "table", - namespace: "", + namespace: "" as string, name: "Second" as string, tableId: resourceToHex({ type: "table", namespace: "", name: "Second" }), schema: { @@ -421,13 +453,24 @@ describe("defineWorld", () => { deploy: TABLE_DEPLOY_DEFAULTS, }, }, + } as const; + + const expectedConfig = { + ...CONFIG_DEFAULTS, + ...expectedBaseNamespace, + namespaces: { + "": { + label: "", + ...expectedBaseNamespace, + }, + }, userTypes: {}, enums: {}, enumValues: {}, - namespace: "", + codegen: CODEGEN_DEFAULTS, } as const; - attest(config).equals(expected); + attest(config).equals(expectedConfig); }); it("should resolve two tables in the config with different schemas and user types", () => { @@ -448,14 +491,13 @@ describe("defineWorld", () => { }, }); - const expected = { - ...CONFIG_DEFAULTS, - codegen: CODEGEN_DEFAULTS, + const expectedBaseNamespace = { + namespace: "" as string, tables: { First: { label: "First", type: "table", - namespace: "", + namespace: "" as string, name: "First" as string, tableId: resourceToHex({ type: "table", namespace: "", name: "First" }), schema: { @@ -479,7 +521,7 @@ describe("defineWorld", () => { Second: { label: "Second", type: "table", - namespace: "", + namespace: "" as string, name: "Second" as string, tableId: resourceToHex({ type: "table", namespace: "", name: "Second" }), schema: { @@ -501,16 +543,27 @@ describe("defineWorld", () => { deploy: TABLE_DEPLOY_DEFAULTS, }, }, + } as const; + + const expectedConfig = { + ...CONFIG_DEFAULTS, + ...expectedBaseNamespace, + namespaces: { + "": { + label: "", + ...expectedBaseNamespace, + }, + }, userTypes: { Static: { type: "address", filePath: "path/to/file" }, Dynamic: { type: "string", filePath: "path/to/file" }, }, enums: {}, enumValues: {}, - namespace: "", + codegen: CODEGEN_DEFAULTS, } as const; - attest(config).equals(expected); + attest(config).equals(expectedConfig); }); it("should throw if referring to fields of different tables", () => { @@ -584,14 +637,14 @@ describe("defineWorld", () => { ValidNames: ["first", "second"], }, }); - const expected = { - ...CONFIG_DEFAULTS, - codegen: CODEGEN_DEFAULTS, + + const expectedBaseNamespace = { + namespace: "" as string, tables: { Example: { label: "Example", type: "table", - namespace: "", + namespace: "" as string, name: "Example" as string, tableId: resourceToHex({ type: "table", namespace: "", name: "Example" }), schema: { @@ -613,6 +666,18 @@ describe("defineWorld", () => { deploy: TABLE_DEPLOY_DEFAULTS, }, }, + } as const; + + const expectedConfig = { + ...CONFIG_DEFAULTS, + ...expectedBaseNamespace, + namespaces: { + "": { + label: "", + ...expectedBaseNamespace, + }, + }, + codegen: CODEGEN_DEFAULTS, userTypes: { static: { type: "address", filePath: "path/to/file" }, dynamic: { type: "string", filePath: "path/to/file" }, @@ -626,23 +691,20 @@ describe("defineWorld", () => { second: 1, }, }, - namespace: "", } as const; - attest(config).equals(expected); - attest(expected).equals(expected); + attest(config).equals(expectedConfig); + attest(expectedConfig).equals(expectedConfig); }); it("should use the root namespace as default namespace", () => { const config = defineWorld({}); - - attest<"">(config.namespace).equals(""); + attest(config.namespace).equals(""); }); it("should use pipe through non-default namespaces", () => { const config = defineWorld({ namespace: "custom" }); - - attest<"custom">(config.namespace).equals("custom"); + attest(config.namespace).equals("custom"); }); it("should extend the output World type", () => { @@ -665,8 +727,8 @@ describe("defineWorld", () => { }, }); - attest<"namespace">(config.namespace).equals("namespace"); - attest<"namespace">(config.tables.namespace__Example.namespace).equals("namespace"); + attest(config.namespace).equals("namespace"); + attest(config.tables.namespace__Example.namespace).equals("namespace"); attest(config.tables.namespace__Example.tableId).equals( resourceToHex({ type: "table", name: "Example", namespace: "namespace" }), ); @@ -776,7 +838,7 @@ describe("defineWorld", () => { const expectedSystems = { Example: { label: "Example", - namespace: "app", + namespace: "app" as string, name: "Example" as string, systemId: resourceToHex({ type: "system", namespace: "app", name: "Example" }), registerFunctionSelectors: true, diff --git a/packages/world/ts/config/v2/world.ts b/packages/world/ts/config/v2/world.ts index 1df4f63184..725940aba3 100644 --- a/packages/world/ts/config/v2/world.ts +++ b/packages/world/ts/config/v2/world.ts @@ -54,7 +54,7 @@ export type resolveWorld["namespace"]> = resolveStore & mergeIfUndefined< { readonly tables: resolveNamespacedTables } & { - [key in Exclude]: key extends "systems" + [key in Exclude]: key extends "systems" ? resolveSystems : key extends "deploy" ? resolveDeploy @@ -69,6 +69,7 @@ export type resolveWorld["namespace"]> = export function resolveWorld(world: world): resolveWorld { const scope = extendedScope(world); + const resolvedStore = resolveStore(world); const namespaces = world.namespaces ?? {}; const resolvedNamespacedTables = Object.fromEntries( @@ -85,8 +86,6 @@ export function resolveWorld(world: world): reso .flat(), ) as Tables; - const resolvedStore = resolveStore(world); - const modules = (world.modules ?? CONFIG_DEFAULTS.modules).map((mod) => mergeIfUndefined(mod, MODULE_DEFAULTS)); return mergeIfUndefined( diff --git a/packages/world/ts/config/v2/worldWithShorthands.test.ts b/packages/world/ts/config/v2/worldWithShorthands.test.ts index 7082166c23..e373c069d8 100644 --- a/packages/world/ts/config/v2/worldWithShorthands.test.ts +++ b/packages/world/ts/config/v2/worldWithShorthands.test.ts @@ -16,6 +16,7 @@ const CODEGEN_DEFAULTS = { ...STORE_CODEGEN_DEFAULTS, ...WORLD_CODEGEN_DEFAULTS describe("defineWorldWithShorthands", () => { it.skip("should resolve namespaced shorthand table config with user types and enums", () => { const config = defineWorldWithShorthands({ + // @ts-expect-error TODO namespaces: { ExampleNS: { tables: { @@ -70,7 +71,7 @@ describe("defineWorldWithShorthands", () => { Second: 1, }, }, - namespace: "", + namespace: "" as string, } as const; attest(config).equals(expected); @@ -78,6 +79,7 @@ describe("defineWorldWithShorthands", () => { it.skip("should resolve namespaced shorthand schema table config with user types and enums", () => { const config = defineWorldWithShorthands({ + // @ts-expect-error TODO namespaces: { ExampleNS: { tables: { @@ -140,7 +142,7 @@ describe("defineWorldWithShorthands", () => { Second: 1, }, }, - namespace: "", + namespace: "" as string, } as const; attest(config).equals(expected); @@ -149,14 +151,13 @@ describe("defineWorldWithShorthands", () => { it("should accept a shorthand store config as input and expand it", () => { const config = defineWorldWithShorthands({ tables: { Name: "address" } }); - const expected = { - ...CONFIG_DEFAULTS, - codegen: CODEGEN_DEFAULTS, + const expectedBaseNamespace = { + namespace: "" as string, tables: { Name: { label: "Name", type: "table", - namespace: "", + namespace: "" as string, name: "Name" as string, tableId: resourceToHex({ type: "table", namespace: "", name: "Name" }), schema: { @@ -174,14 +175,25 @@ describe("defineWorldWithShorthands", () => { deploy: TABLE_DEPLOY_DEFAULTS, }, }, + } as const; + + const expectedConfig = { + ...CONFIG_DEFAULTS, + ...expectedBaseNamespace, + namespaces: { + "": { + label: "", + ...expectedBaseNamespace, + }, + }, userTypes: {}, enums: {}, enumValues: {}, - namespace: "", + codegen: CODEGEN_DEFAULTS, } as const; - attest(config).equals(expected); - attest(expected); + attest(config).equals(expectedConfig); + attest(expectedConfig); }); it("should accept a user type as input and expand it", () => { @@ -189,14 +201,14 @@ describe("defineWorldWithShorthands", () => { tables: { Name: "CustomType" }, userTypes: { CustomType: { type: "address", filePath: "path/to/file" } }, }); - const expected = { - ...CONFIG_DEFAULTS, - codegen: CODEGEN_DEFAULTS, + + const expectedBaseNamespace = { + namespace: "" as string, tables: { Name: { label: "Name", type: "table", - namespace: "", + namespace: "" as string, name: "Name" as string, tableId: resourceToHex({ type: "table", namespace: "", name: "Name" }), schema: { @@ -214,27 +226,38 @@ describe("defineWorldWithShorthands", () => { deploy: TABLE_DEPLOY_DEFAULTS, }, }, + } as const; + + const expectedConfig = { + ...CONFIG_DEFAULTS, + ...expectedBaseNamespace, + namespaces: { + "": { + label: "", + ...expectedBaseNamespace, + }, + }, userTypes: { CustomType: { type: "address", filePath: "path/to/file" as string } }, enums: {}, enumValues: {}, - namespace: "", + codegen: CODEGEN_DEFAULTS, } as const; - attest(config).equals(expected); + attest(config).equals(expectedConfig); }); it("given a schema with a key field with static ABI type, it should use `id` as single key", () => { const config = defineWorldWithShorthands({ tables: { Example: { id: "address", name: "string", age: "uint256" } }, }); - const expected = { - ...CONFIG_DEFAULTS, - codegen: CODEGEN_DEFAULTS, + + const expectedBaseNamespace = { + namespace: "" as string, tables: { Example: { label: "Example", type: "table", - namespace: "", + namespace: "" as string, name: "Example" as string, tableId: resourceToHex({ type: "table", namespace: "", name: "Example" }), schema: { @@ -256,27 +279,38 @@ describe("defineWorldWithShorthands", () => { deploy: TABLE_DEPLOY_DEFAULTS, }, }, + } as const; + + const expectedConfig = { + ...CONFIG_DEFAULTS, + ...expectedBaseNamespace, + namespaces: { + "": { + label: "", + ...expectedBaseNamespace, + }, + }, userTypes: {}, enums: {}, enumValues: {}, - namespace: "", + codegen: CODEGEN_DEFAULTS, } as const; - attest(config).equals(expected); + attest(config).equals(expectedConfig); }); it("given a schema with a key field with static custom type, it should use `id` as single key", () => { const config = defineWorldWithShorthands({ tables: { Example: { id: "address", name: "string", age: "uint256" } }, }); - const expected = { - ...CONFIG_DEFAULTS, - codegen: CODEGEN_DEFAULTS, + + const expectedBaseNamespace = { + namespace: "" as string, tables: { Example: { label: "Example", type: "table", - namespace: "", + namespace: "" as string, name: "Example" as string, tableId: resourceToHex({ type: "table", namespace: "", name: "Example" }), schema: { @@ -298,13 +332,24 @@ describe("defineWorldWithShorthands", () => { deploy: TABLE_DEPLOY_DEFAULTS, }, }, + } as const; + + const expectedConfig = { + ...CONFIG_DEFAULTS, + ...expectedBaseNamespace, + namespaces: { + "": { + label: "", + ...expectedBaseNamespace, + }, + }, userTypes: {}, enums: {}, enumValues: {}, - namespace: "", + codegen: CODEGEN_DEFAULTS, } as const; - attest(config).equals(expected); + attest(config).equals(expectedConfig); }); it("throw an error if the shorthand doesn't include a key field", () => { @@ -363,10 +408,10 @@ describe("defineWorldWithShorthands", () => { it.skip("should throw with an invalid namespace config option", () => { attest(() => defineWorldWithShorthands({ + // @ts-expect-error TODO namespaces: { ExampleNS: { tables: { - // @ts-expect-error Type '"number"' is not assignable to type 'AbiType'. ExampleTable: "number", }, }, @@ -378,9 +423,9 @@ describe("defineWorldWithShorthands", () => { it.skip("should throw with a non-existent namespace config option", () => { attest(() => defineWorldWithShorthands({ + // @ts-expect-error TODO namespaces: { ExampleNS: { - // @ts-expect-error Type 'true' is not assignable to type '"`invalidProperty` is not a valid namespace config option. invalidProperty: true, }, }, diff --git a/packages/world/ts/config/v2/worldWithShorthands.ts b/packages/world/ts/config/v2/worldWithShorthands.ts index 948331da67..f8f4339660 100644 --- a/packages/world/ts/config/v2/worldWithShorthands.ts +++ b/packages/world/ts/config/v2/worldWithShorthands.ts @@ -1,7 +1,5 @@ import { mapObject } from "@latticexyz/common/utils"; import { - AbiTypeScope, - Scope, extendedScope, getPath, hasOwnKey, @@ -12,29 +10,12 @@ import { validateTablesWithShorthands, } from "@latticexyz/store/config/v2"; import { WorldWithShorthandsInput } from "./input"; -import { validateNamespace } from "./namespaces"; import { resolveWorld, validateWorld } from "./world"; -export type resolveWorldWithShorthands = resolveWorld<{ - [key in keyof world]: key extends "tables" - ? resolveTablesWithShorthands> - : key extends "namespaces" - ? { - [namespaceKey in keyof world[key]]: { - [namespaceProp in keyof world[key][namespaceKey]]: namespaceProp extends "tables" - ? resolveTablesWithShorthands> - : world[key][namespaceKey][namespaceProp]; - }; - } - : world[key]; -}>; - export type validateWorldWithShorthands = { [key in keyof world]: key extends "tables" ? validateTablesWithShorthands> - : key extends "namespaces" - ? validateNamespacesWithShorthands> - : validateWorld[key]; + : validateWorld[key]; }; function validateWorldWithShorthands(world: unknown): asserts world is WorldWithShorthandsInput { @@ -50,13 +31,11 @@ function validateWorldWithShorthands(world: unknown): asserts world is WorldWith } } -export type validateNamespacesWithShorthands = { - [namespace in keyof namespaces]: { - [key in keyof namespaces[namespace]]: key extends "tables" - ? validateTablesWithShorthands - : validateNamespace[key]; - }; -}; +export type resolveWorldWithShorthands = resolveWorld<{ + readonly [key in keyof world]: key extends "tables" + ? resolveTablesWithShorthands> + : world[key]; +}>; export function resolveWorldWithShorthands( world: world, @@ -65,14 +44,8 @@ export function resolveWorldWithShorthands { return isTableShorthandInput(table) ? resolveTableShorthand(table, scope) : table; }); - const namespaces = mapObject(world.namespaces ?? {}, (namespace) => ({ - ...namespace, - tables: mapObject(namespace.tables ?? {}, (table) => { - return isTableShorthandInput(table) ? resolveTableShorthand(table, scope) : table; - }), - })); - const fullConfig = { ...world, tables, namespaces }; + const fullConfig = { ...world, tables }; validateWorld(fullConfig); return resolveWorld(fullConfig) as never;