diff --git a/src/components/Data/TypeLoader.ts b/src/components/Data/TypeLoader.ts index f3a8cbdf8..61d2729df 100644 --- a/src/components/Data/TypeLoader.ts +++ b/src/components/Data/TypeLoader.ts @@ -3,6 +3,7 @@ import { IDisposable } from '/@/types/disposable' import { DataLoader } from './DataLoader' import { Tab } from '../TabSystem/CommonTab' import { FileTab } from '../TabSystem/FileTab' +import { toTypeDefinition } from '../JSONSchema/ToTypes/main' import { IRequirements, RequiresMatcher, @@ -69,6 +70,13 @@ export class TypeLoader { await App.fileType.ready.fired const { types = [] } = App.fileType.get(filePath) ?? {} + // console.log( + // ...toTypeDefinition( + // App.fileType.all.filter( + // (fileType) => fileType.id === 'lootTable' + // )[0]?.schema + // ) + // ) const matcher = new RequiresMatcher() await matcher.setup() diff --git a/src/components/JSONSchema/Schema/AllOf.ts b/src/components/JSONSchema/Schema/AllOf.ts index 644cb40ac..09dcfc06c 100644 --- a/src/components/JSONSchema/Schema/AllOf.ts +++ b/src/components/JSONSchema/Schema/AllOf.ts @@ -1,6 +1,7 @@ import { ParentSchema } from './Parent' import { RootSchema } from './Root' import { Schema } from './Schema' +import { UnionType } from '../ToTypes/Union' export class AllOfSchema extends ParentSchema { protected children: Schema[] diff --git a/src/components/JSONSchema/Schema/Const.ts b/src/components/JSONSchema/Schema/Const.ts index d5652284f..8e6456f95 100644 --- a/src/components/JSONSchema/Schema/Const.ts +++ b/src/components/JSONSchema/Schema/Const.ts @@ -1,3 +1,4 @@ +import { LiteralType } from '../ToTypes/Literal' import { Schema } from './Schema' export class ConstSchema extends Schema { @@ -21,4 +22,8 @@ export class ConstSchema extends Schema { ] return [] } + + override toTypeDefinition() { + return new LiteralType(this.value) + } } diff --git a/src/components/JSONSchema/Schema/ElseSchema.ts b/src/components/JSONSchema/Schema/ElseSchema.ts index c9f56ec9a..8a27f3b29 100644 --- a/src/components/JSONSchema/Schema/ElseSchema.ts +++ b/src/components/JSONSchema/Schema/ElseSchema.ts @@ -38,4 +38,8 @@ export class ElseSchema extends Schema { if (!this.ifSchema.isTrue(obj)) return this.rootSchema.validate(obj) return [] } + + override toTypeDefinition(hoisted: Set) { + return this.rootSchema.toTypeDefinition(hoisted) + } } diff --git a/src/components/JSONSchema/Schema/Enum.ts b/src/components/JSONSchema/Schema/Enum.ts index e0c167729..73b51023b 100644 --- a/src/components/JSONSchema/Schema/Enum.ts +++ b/src/components/JSONSchema/Schema/Enum.ts @@ -1,3 +1,5 @@ +import { LiteralType } from '../ToTypes/Literal' +import { UnionType } from '../ToTypes/Union' import { Schema } from './Schema' export class EnumSchema extends Schema { @@ -31,4 +33,10 @@ export class EnumSchema extends Schema { ] return [] } + + override toTypeDefinition() { + if(!Array.isArray(this.value)) return null + + return new UnionType(this.value.map(val => new LiteralType(val))) + } } diff --git a/src/components/JSONSchema/Schema/Items.ts b/src/components/JSONSchema/Schema/Items.ts index 5290ea8e2..9f08d5638 100644 --- a/src/components/JSONSchema/Schema/Items.ts +++ b/src/components/JSONSchema/Schema/Items.ts @@ -1,5 +1,8 @@ import { RootSchema } from './Root' import { IDiagnostic, Schema } from './Schema' +import { TupleType } from '../ToTypes/Tuple' +import { ArrayType } from '../ToTypes/Array' +import { BaseType } from '../ToTypes/Type' export class ItemsSchema extends Schema { protected children: RootSchema | RootSchema[] @@ -70,4 +73,22 @@ export class ItemsSchema extends Schema { validate(obj: unknown) { return [] } + + override toTypeDefinition(hoisted: Set) { + if (Array.isArray(this.children)) { + return new TupleType( + this.children + .filter((child) => !child.hasDoNotSuggest) + .map((child) => child.toTypeDefinition(hoisted)) + .filter((type) => type !== null) + ) + } else { + if(this.children.hasDoNotSuggest) return null + + const type = this.children.toTypeDefinition(hoisted) + if (type === null) return null + + return new ArrayType(type) + } + } } diff --git a/src/components/JSONSchema/Schema/Parent.ts b/src/components/JSONSchema/Schema/Parent.ts index db96daa32..70bda7dc9 100644 --- a/src/components/JSONSchema/Schema/Parent.ts +++ b/src/components/JSONSchema/Schema/Parent.ts @@ -1,3 +1,5 @@ +import { BaseType } from '../ToTypes/Type' +import { UnionType } from '../ToTypes/Union' import { DoNotSuggestSchema } from './DoNotSuggest' import { Schema } from './Schema' @@ -39,4 +41,14 @@ export abstract class ParentSchema extends Schema { return children } + + override toTypeDefinition(hoisted: Set) { + if(this.hasDoNotSuggest) console.warn(`[${this.location}] Called Schema.toTypeDefinition on a schema which has "doNotSuggest"`) + + return new UnionType( + (this.children + .map((child) => child.toTypeDefinition(hoisted)) + .filter((schema) => schema !== null)) + ) + } } diff --git a/src/components/JSONSchema/Schema/Properties.ts b/src/components/JSONSchema/Schema/Properties.ts index cd855b84c..350bddd6d 100644 --- a/src/components/JSONSchema/Schema/Properties.ts +++ b/src/components/JSONSchema/Schema/Properties.ts @@ -1,4 +1,5 @@ import { RootSchema } from './Root' +import { InterfaceType } from '../ToTypes/Interface' import { ICompletionItem, IDiagnostic, Schema } from './Schema' export class PropertiesSchema extends Schema { @@ -100,4 +101,19 @@ export class PropertiesSchema extends Schema { return diagnostics } + + override toTypeDefinition(hoisted: Set) { + const interfaceType = new InterfaceType() + + for (const [propertyName, child] of Object.entries(this.children)) { + if (child.hasDoNotSuggest) continue + + const type = child.toTypeDefinition(hoisted) + if (type === null) continue + + interfaceType.addProperty(propertyName, type) + } + + return interfaceType + } } diff --git a/src/components/JSONSchema/Schema/Ref.ts b/src/components/JSONSchema/Schema/Ref.ts index 2e3c2427f..1477fad18 100644 --- a/src/components/JSONSchema/Schema/Ref.ts +++ b/src/components/JSONSchema/Schema/Ref.ts @@ -1,7 +1,9 @@ import { SchemaManager } from '../Manager' +import { pathToName } from '../pathToName' +import { PrimitiveType } from '../ToTypes/Primitive' import { RootSchema } from './Root' import { Schema } from './Schema' -import { dirname, join } from '/@/utils/path' +import { dirname, join, relative } from '/@/utils/path' export class RefSchema extends Schema { public readonly schemaType = 'refSchema' @@ -74,4 +76,22 @@ export class RefSchema extends Schema { getFreeIfSchema() { return this.rootSchema.getFreeIfSchema() } + + getName() { + return pathToName( + relative( + 'file:///data/packages/minecraftBedrock/schema', + this.rootSchema.getLocation() + ) + ) + } + + override toTypeDefinition(hoisted: Set, forceEval?: boolean) { + if(forceEval) return this.rootSchema.toTypeDefinition(hoisted) + + if(!hoisted.has(this)) + hoisted.add(this) + + return new PrimitiveType(this.getName()) + } } diff --git a/src/components/JSONSchema/Schema/Schema.ts b/src/components/JSONSchema/Schema/Schema.ts index 08886d005..7eb5d0fb0 100644 --- a/src/components/JSONSchema/Schema/Schema.ts +++ b/src/components/JSONSchema/Schema/Schema.ts @@ -1,3 +1,7 @@ +import { pathToName } from '../pathToName' +import { BaseType } from '../ToTypes/Type' +import { relative } from '/@/utils/path' + export interface ISchemaResult { diagnostics: IDiagnostic[] } @@ -51,4 +55,22 @@ export abstract class Schema { obj: unknown, location: (string | number | undefined)[] ): Schema[] + + toTypeDefinition( + hoisted: Set, + forceEval?: boolean + ): BaseType | null { + return null + } + getName() { + return pathToName( + relative( + 'file:///data/packages/minecraftBedrock/schema', + this.location + ) + ) + } + getLocation() { + return this.location + } } diff --git a/src/components/JSONSchema/Schema/ThenSchema.ts b/src/components/JSONSchema/Schema/ThenSchema.ts index e5352cd59..ee1aa9cdd 100644 --- a/src/components/JSONSchema/Schema/ThenSchema.ts +++ b/src/components/JSONSchema/Schema/ThenSchema.ts @@ -38,4 +38,8 @@ export class ThenSchema extends Schema { if (this.ifSchema.isTrue(obj)) return this.rootSchema.validate(obj) return [] } + + override toTypeDefinition(hoisted: Set) { + return this.rootSchema.toTypeDefinition(hoisted) + } } diff --git a/src/components/JSONSchema/Schema/Type.ts b/src/components/JSONSchema/Schema/Type.ts index 9779ff8b8..1298b9d47 100644 --- a/src/components/JSONSchema/Schema/Type.ts +++ b/src/components/JSONSchema/Schema/Type.ts @@ -1,5 +1,7 @@ import { ICompletionItem, Schema } from './Schema' import { getTypeOf } from '/@/utils/typeof' +import { PrimitiveType, TSupportedPrimitiveTypes } from '../ToTypes/Primitive' +import { UnionType } from '../ToTypes/Union' export class TypeSchema extends Schema { get values() { @@ -60,4 +62,16 @@ export class TypeSchema extends Schema { return [] } + + override toTypeDefinition() { + const values = ( + Array.isArray(this.value) ? this.value : [this.value] + ).filter((type) => !['object', 'array'].includes(type)) + + return new UnionType( + values.map( + (val) => new PrimitiveType(PrimitiveType.toTypeScriptType(val)) + ) + ) + } } diff --git a/src/components/JSONSchema/ToTypes/Array.ts b/src/components/JSONSchema/ToTypes/Array.ts new file mode 100644 index 000000000..089a1c064 --- /dev/null +++ b/src/components/JSONSchema/ToTypes/Array.ts @@ -0,0 +1,11 @@ +import { BaseType } from './Type' + +export class ArrayType extends BaseType { + constructor(public readonly type: BaseType) { + super() + } + + public toString() { + return `${this.type.toString()}[]` + } +} diff --git a/src/components/JSONSchema/ToTypes/CombinedType.ts b/src/components/JSONSchema/ToTypes/CombinedType.ts new file mode 100644 index 000000000..65f5747e6 --- /dev/null +++ b/src/components/JSONSchema/ToTypes/CombinedType.ts @@ -0,0 +1,71 @@ +import { InterfaceType } from './Interface' +import { BaseType } from './Type' + +export abstract class CombinedType extends BaseType { + constructor(private types: BaseType[], protected joinChar: string) { + super() + } + + protected abstract isOfThisType(type: CombinedType): boolean + + add(baseType: BaseType) { + this.types.push(baseType) + } + + protected flatten(): BaseType[] { + return this.types + .map((type) => { + if (type instanceof CombinedType) { + const flat = type.flatten() + + if (flat.length === 0) return [] + else if (flat.length === 1) return flat[0] + else if (this.isOfThisType(type)) return flat + + return type + } else return type + }) + .flat(1) + } + + protected withCollapsedInterfaces() { + const types = this.flatten() + const interfaceType = new InterfaceType() + + const newTypes: BaseType[] = [] + let foundInterface = false + for (const type of types) { + if (type instanceof InterfaceType) { + interfaceType.addFrom(type) + foundInterface = true + } else { + newTypes.push(type) + } + } + + return foundInterface ? newTypes.concat(interfaceType) : newTypes + } + + isStringCollection(collection: string[]) { + return collection.every( + (type) => + (type.startsWith("'") && type.endsWith("'")) || + type === 'string' + ) + } + + toString() { + let collection = [ + ...new Set( + this.withCollapsedInterfaces().map((type) => type.toString()) + ), + ] + + if (this.isStringCollection(collection)) + collection = collection.filter((type) => type !== 'string') + + if (collection.length === 1) return collection[0] + + return `(${collection.join(this.joinChar)})` + } +} diff --git a/src/components/JSONSchema/ToTypes/Interface.ts b/src/components/JSONSchema/ToTypes/Interface.ts new file mode 100644 index 000000000..0403bd71e --- /dev/null +++ b/src/components/JSONSchema/ToTypes/Interface.ts @@ -0,0 +1,33 @@ +import { CombinedType } from './CombinedType' +import { BaseType } from './Type' +import { UnionType } from './Union' + +export class InterfaceType extends BaseType { + protected properties: Record = {} + + addProperty(name: string, type: BaseType) { + this.properties[name] = type + } + addFrom(interfaceType: InterfaceType) { + for (const [key, type] of Object.entries(interfaceType.properties)) { + const existingType = this.properties[key] + if(existingType instanceof CombinedType) { + existingType.add(type) + } else if(existingType) { + this.addProperty(key, new UnionType([this.properties[key], type])) + } else { + this.addProperty(key, type) + } + } + } + + toString() { + return `{ ${Object.entries(this.properties) + .map(([key, type]) => `${key}?: ${type.toString()}`) + .join(';')};[k: string]: unknown; }` + } + + override withName(name: string) { + return `interface ${name} ${this.toString()};` + } +} \ No newline at end of file diff --git a/src/components/JSONSchema/ToTypes/Intersection.ts b/src/components/JSONSchema/ToTypes/Intersection.ts new file mode 100644 index 000000000..7bdc41882 --- /dev/null +++ b/src/components/JSONSchema/ToTypes/Intersection.ts @@ -0,0 +1,12 @@ +import { BaseType } from './Type' +import { CombinedType } from './CombinedType' + +export class IntersectionType extends CombinedType { + constructor(types: BaseType[]) { + super(types, ' & ') + } + + isOfThisType(type: CombinedType) { + return type instanceof IntersectionType + } +} diff --git a/src/components/JSONSchema/ToTypes/Literal.ts b/src/components/JSONSchema/ToTypes/Literal.ts new file mode 100644 index 000000000..ff2230278 --- /dev/null +++ b/src/components/JSONSchema/ToTypes/Literal.ts @@ -0,0 +1,12 @@ +import { BaseType } from './Type' + +export class LiteralType extends BaseType { + constructor(protected literalType: string | number | boolean) { + super() + } + + public toString() { + if (typeof this.literalType === 'string') return `'${this.literalType}'` + return `${this.literalType}` + } +} diff --git a/src/components/JSONSchema/ToTypes/Primitive.ts b/src/components/JSONSchema/ToTypes/Primitive.ts new file mode 100644 index 000000000..883ce49fc --- /dev/null +++ b/src/components/JSONSchema/ToTypes/Primitive.ts @@ -0,0 +1,22 @@ +import { BaseType } from './Type' + +export type TSupportedPrimitiveTypes = 'string' | 'number' | 'boolean' | 'null' + +export class PrimitiveType extends BaseType { + constructor(protected primitiveType: TSupportedPrimitiveTypes | string) { + super() + } + + static toTypeScriptType( + jsonSchemaType: 'boolean' | 'string' | 'decimal' | 'integer' | 'null' + ) { + if (jsonSchemaType === 'decimal' || jsonSchemaType === 'integer') + return 'number' + + return jsonSchemaType + } + + public toString() { + return this.primitiveType + } +} diff --git a/src/components/JSONSchema/ToTypes/Tuple.ts b/src/components/JSONSchema/ToTypes/Tuple.ts new file mode 100644 index 000000000..6bf29314b --- /dev/null +++ b/src/components/JSONSchema/ToTypes/Tuple.ts @@ -0,0 +1,11 @@ +import { BaseType } from './Type' + +export class TupleType extends BaseType { + constructor(public readonly types: BaseType[]) { + super() + } + + public toString() { + return `[${this.types.map((type) => type.toString()).join(', ')}]` + } +} diff --git a/src/components/JSONSchema/ToTypes/Type.ts b/src/components/JSONSchema/ToTypes/Type.ts new file mode 100644 index 000000000..58035b296 --- /dev/null +++ b/src/components/JSONSchema/ToTypes/Type.ts @@ -0,0 +1,7 @@ +export abstract class BaseType { + public abstract toString(): string + + withName(name: string) { + return `type ${name} = ${this.toString()};` + } +} diff --git a/src/components/JSONSchema/ToTypes/Union.ts b/src/components/JSONSchema/ToTypes/Union.ts new file mode 100644 index 000000000..79dc240e4 --- /dev/null +++ b/src/components/JSONSchema/ToTypes/Union.ts @@ -0,0 +1,12 @@ +import { BaseType } from '../ToTypes/Type' +import { CombinedType } from './CombinedType' + +export class UnionType extends CombinedType { + constructor(types: BaseType[]) { + super(types, ' | ') + } + + isOfThisType(type: CombinedType) { + return type instanceof UnionType + } +} diff --git a/src/components/JSONSchema/ToTypes/main.ts b/src/components/JSONSchema/ToTypes/main.ts new file mode 100644 index 000000000..b1b80ca28 --- /dev/null +++ b/src/components/JSONSchema/ToTypes/main.ts @@ -0,0 +1,22 @@ +import { requestOrCreateSchema } from '../requestOrCreateSchema' +import { Schema } from '../Schema/Schema' + +export function toTypeDefinition(fileUri: string): [string, string] { + const hoisted = new Set() + + const definition = requestOrCreateSchema(fileUri) + ?.toTypeDefinition(hoisted) + .toString() + + let hoistedDefinitions = new Set() + + for (const schema of hoisted) { + hoistedDefinitions.add( + schema.toTypeDefinition(hoisted, true)?.withName(schema.getName()) + ) + + console.log(schema.getName(), schema.toTypeDefinition(hoisted, true)) + } + + return [[...hoistedDefinitions].join(''), definition] +} diff --git a/src/components/JSONSchema/pathToName.ts b/src/components/JSONSchema/pathToName.ts new file mode 100644 index 000000000..b1e890511 --- /dev/null +++ b/src/components/JSONSchema/pathToName.ts @@ -0,0 +1,13 @@ +export function pathToName(path: string): string { + const parts = path.replaceAll('#', '').replaceAll('_', '/').split(/\\|\//g) + + // Capitalize the first letter of each part + const capitalized = parts.map( + (part) => part.charAt(0).toUpperCase() + part.slice(1) + ) + + // Remove file extensions from parts (they can be anywhere because of the "#/" $ref hash syntax) + const withoutExts = capitalized.map((part) => part.split('.')[0]) + + return withoutExts.join('') +} diff --git a/src/components/JSONSchema/requestOrCreateSchema.ts b/src/components/JSONSchema/requestOrCreateSchema.ts new file mode 100644 index 000000000..4514d0a62 --- /dev/null +++ b/src/components/JSONSchema/requestOrCreateSchema.ts @@ -0,0 +1,13 @@ +import { SchemaManager } from './Manager' +import { RootSchema } from './Schema/Root' + +export function requestOrCreateSchema(fileUri: string) { + let schema = SchemaManager.requestRootSchema(fileUri) + if (schema) return schema + + const schemaDef = SchemaManager.request(fileUri) + schema = new RootSchema(fileUri, '$global', schemaDef) + SchemaManager.addRootSchema(fileUri, schema) + + return schema +} diff --git a/src/components/Projects/Project/Project.ts b/src/components/Projects/Project/Project.ts index 18ef9630d..b3da3d348 100644 --- a/src/components/Projects/Project/Project.ts +++ b/src/components/Projects/Project/Project.ts @@ -243,11 +243,6 @@ export abstract class Project { await this.extensionLoader.loadExtensions() - const selectedTab = this.tabSystem?.selectedTab - this.typeLoader.activate( - selectedTab instanceof FileTab ? selectedTab.getPath() : undefined - ) - // Data needs to be loaded into IndexedDB before the PackIndexer can be used await this.app.dataLoader.fired @@ -262,8 +257,18 @@ export abstract class Project { (settingsState.compiler?.autoFetchChangedFiles ?? true) && !this.isVirtualProject + const selectedTab = this.tabSystem?.selectedTab + await Promise.all([ - this.jsonDefaults.activate(), + this.jsonDefaults + .activate() + .finally(() => + this.typeLoader.activate( + selectedTab instanceof FileTab + ? selectedTab.getPath() + : undefined + ) + ), autoFetchChangedFiles ? this.compilerService.start(changedFiles, deletedFiles) : Promise.resolve(),