diff --git a/README.md b/README.md
index 18404e3..2bb3332 100644
--- a/README.md
+++ b/README.md
@@ -72,7 +72,7 @@ __*Subcommands*__
#### Init Root
-Generate an empty standard file structure.
+Generate an empty standard file structure. JSON schemas will also be generated.
`init root`
@@ -139,6 +139,14 @@ Options:
---
+#### Generate Schemas
+
+Generate the JSON schemas used by Nebula's internal types (ex. Distro Meta and Server Meta schemas). This command should be used to update the schemas when a change to Nebula requires it. You may need to reopen your editor for the new JSON schemas to take effect.
+
+`generate schemas`
+
+---
+
### Latest Forge
Get the latest version of Forge.
@@ -200,14 +208,19 @@ If a directory represents a toggleable mod, it will have three subdirectories. Y
### Additional Metadata
-To preserve metadata that cannot be inferred via file structure, two files exist. Default values will be generated when applicable. Customize to fit your needs. These values should be self explanatory. If further details are required, see the [distribution.json specification document][distro.md].
+To preserve metadata that cannot be inferred via file structure, two files exist. Default values will be generated when applicable. Customize to fit your needs. These values should be self explanatory. If further details are required, see the [distribution.json specification document][distro.md].
#### ${ROOT}/meta/distrometa.json
-Represents the additiona metadata on the distribution object. Sample:
+Represents the additiona metadata on the distribution object.
+
+A JSON schema is provided to assist editing this file. It should automatically be referenced when the default file is generated.
+
+Sample:
```json
{
+ "$schema": "file:///${ROOT}/schemas/DistroMetaSchema.schema.json",
"meta": {
"rss": "",
"discord": {
@@ -221,10 +234,15 @@ Represents the additiona metadata on the distribution object. Sample:
#### servers/${YOUR_SERVER}/servermeta.json
-Represents the additional metadata on the server object (for a YOUR_SERVER). Sample:
+Represents the additional metadata on the server object (for a YOUR_SERVER).
+
+A JSON schema is provided to assist editing this file. It should automatically be referenced when the default file is generated.
+
+Sample:
```json
{
+ "$schema": "file:///${ROOT}/schemas/ServerMetaSchema.schema.json",
"meta": {
"version": "1.0.0",
"name": "Test (Minecraft 1.12.2)",
@@ -285,6 +303,13 @@ In the above example, all files of type `cfg` in the config directory will be un
Another example where all `optionalon` forgemods and litemods are untracked. **Untracking mods is NOT recommended. This is an example ONLY.**
+### Note on JSON Schemas
+
+The `$schema` property in a JSON file is a URL to a JSON schema file. This property is optional. Nebula provides schemas for internal types to make editing the JSON easier. Editors, such as Visual Studio Code, will use this schema file to validate the data and show useful information, like property descriptions. Valid properties will also be autocompleted. For detailed information, you may view the [JSON Schema Website](jsonschemawebsite).
+
+Nebula will store JSON schemas in `${ROOT}/schemas`. This is so that they will always be in sync with your local version of Nebula. They will initially be generated by the `init root` command. To update the schemas, you can run the `generate schemas` command.
+
[dotenvnpm]: https://www.npmjs.com/package/dotenv
-[distro.md]: https://github.com/dscalzi/HeliosLauncher/blob/master/docs/distro.md
\ No newline at end of file
+[distro.md]: https://github.com/dscalzi/HeliosLauncher/blob/master/docs/distro.md
+[jsonschemawebsite]: https://json-schema.org/
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index 492083c..165c710 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -175,8 +175,7 @@
"@types/json-schema": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.6.tgz",
- "integrity": "sha512-3c+yGKvVP5Y9TYBEibGNR+kLtijnj7mYrXRg+WpFb2X9xm04g/DXYkfg4hmzJQosc9snFNUPkbYIhu+KAm6jJw==",
- "dev": true
+ "integrity": "sha512-3c+yGKvVP5Y9TYBEibGNR+kLtijnj7mYrXRg+WpFb2X9xm04g/DXYkfg4hmzJQosc9snFNUPkbYIhu+KAm6jJw=="
},
"@types/keyv": {
"version": "3.1.1",
@@ -537,6 +536,11 @@
"text-hex": "1.0.x"
}
},
+ "commander": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-6.1.0.tgz",
+ "integrity": "sha512-wl7PNrYWd2y5mp1OK/LhTlv8Ff4kQJQRXXAvF+uU/TPNiVJUxZLRYGj/B0y/lPGAVcSbJqH2Za/cvHmrPMC8mA=="
+ },
"concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -933,8 +937,7 @@
"fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
- "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
- "dev": true
+ "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
},
"functional-red-black-tree": {
"version": "1.0.1",
@@ -959,7 +962,6 @@
"version": "7.1.6",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
"integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
- "dev": true,
"requires": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
@@ -1075,7 +1077,6 @@
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
- "dev": true,
"requires": {
"once": "^1.3.0",
"wrappy": "1"
@@ -1160,6 +1161,14 @@
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
"dev": true
},
+ "json-stable-stringify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz",
+ "integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=",
+ "requires": {
+ "jsonify": "~0.0.0"
+ }
+ },
"json-stable-stringify-without-jsonify": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
@@ -1175,6 +1184,11 @@
"universalify": "^1.0.0"
}
},
+ "jsonify": {
+ "version": "0.0.0",
+ "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz",
+ "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM="
+ },
"keyv": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.0.1.tgz",
@@ -1346,8 +1360,7 @@
"path-is-absolute": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
- "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
- "dev": true
+ "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18="
},
"path-key": {
"version": "3.1.1",
@@ -1682,6 +1695,18 @@
"resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.3.0.tgz",
"integrity": "sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw=="
},
+ "ts-json-schema-generator": {
+ "version": "0.73.0",
+ "resolved": "https://registry.npmjs.org/ts-json-schema-generator/-/ts-json-schema-generator-0.73.0.tgz",
+ "integrity": "sha512-qcKsEfEuA2FTOUmrjGangKlKhJww9OOKGeqgz9e/xT/F0iI2sV4VZaahd53Dk4rnzhc0NzfYTQEu8h8NAEleJg==",
+ "requires": {
+ "@types/json-schema": "^7.0.6",
+ "commander": "~6.1.0",
+ "glob": "~7.1.6",
+ "json-stable-stringify": "^1.0.1",
+ "typescript": "~4.0.2"
+ }
+ },
"tslib": {
"version": "1.13.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz",
@@ -1715,8 +1740,7 @@
"typescript": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.0.2.tgz",
- "integrity": "sha512-e4ERvRV2wb+rRZ/IQeb3jm2VxBsirQLpQhdxplZ2MEzGvDkkMmPglecnNDfSUBivMjP93vRbngYYDQqQ/78bcQ==",
- "dev": true
+ "integrity": "sha512-e4ERvRV2wb+rRZ/IQeb3jm2VxBsirQLpQhdxplZ2MEzGvDkkMmPglecnNDfSUBivMjP93vRbngYYDQqQ/78bcQ=="
},
"universalify": {
"version": "1.0.0",
diff --git a/package.json b/package.json
index 9584735..c457197 100644
--- a/package.json
+++ b/package.json
@@ -47,6 +47,7 @@
"node-stream-zip": "^1.11.3",
"toml": "^3.0.0",
"triple-beam": "^1.3.0",
+ "ts-json-schema-generator": "^0.73.0",
"winston": "^3.3.3",
"yargs": "^16.0.3"
}
diff --git a/src/index.ts b/src/index.ts
index 4c4c6c1..2206ce2 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -11,6 +11,7 @@ import { VersionSegmentedRegistry } from './util/VersionSegmentedRegistry'
import { VersionUtil } from './util/versionutil'
import { MinecraftVersion } from './util/MinecraftVersion'
import { LoggerUtil } from './util/LoggerUtil'
+import { generateSchemas } from './util/SchemaUtil'
dotenv.config()
@@ -109,6 +110,7 @@ const initRootCommand: yargs.CommandModule = {
logger.debug(`Root set to ${argv.root}`)
logger.debug('Invoked init root.')
try {
+ await generateSchemas(argv.root as string)
await new DistributionStructure(argv.root as string, '').init()
logger.info(`Successfully created new root at ${argv.root}`)
} catch (error) {
@@ -238,6 +240,25 @@ const generateDistroCommand: yargs.CommandModule = {
}
}
+const generateSchemasCommand: yargs.CommandModule = {
+ command: 'schemas',
+ describe: 'Generate json schemas.',
+ handler: async (argv) => {
+ argv.root = getRoot()
+
+ logger.debug(`Root set to ${argv.root}`)
+ logger.debug('Invoked generate schemas.')
+
+ try {
+ await generateSchemas(argv.root as string)
+ logger.info('Successfully generated schemas')
+
+ } catch (error) {
+ logger.error(`Failed to generate schemas with root ${argv.root}.`, error)
+ }
+ }
+}
+
const generateCommand: yargs.CommandModule = {
command: 'generate',
aliases: ['g'],
@@ -246,6 +267,7 @@ const generateCommand: yargs.CommandModule = {
return yargs
.command(generateServerCommand)
.command(generateDistroCommand)
+ .command(generateSchemasCommand)
},
handler: (argv) => {
argv._handled = true
diff --git a/src/model/nebula/distrometa.ts b/src/model/nebula/distrometa.ts
index 0855292..0817fc3 100644
--- a/src/model/nebula/distrometa.ts
+++ b/src/model/nebula/distrometa.ts
@@ -2,6 +2,9 @@ import { Distribution } from 'helios-distribution-types'
export interface DistroMeta {
+ /**
+ * Distribution metadata to be forwarded to the distribution file.
+ */
meta: {
rss: Distribution['rss']
discord?: Distribution['discord']
diff --git a/src/model/nebula/servermeta.ts b/src/model/nebula/servermeta.ts
index 8949a59..cb8bc1d 100644
--- a/src/model/nebula/servermeta.ts
+++ b/src/model/nebula/servermeta.ts
@@ -2,8 +2,8 @@ import { Server } from 'helios-distribution-types'
export interface UntrackedFilesOption {
/**
- * The subdirectory this applies to. Ex.
- * [ 'files', 'forgemods' ]
+ * The subdirectory these patterns will be applied to. Ex.
+ * [ "files", "forgegemods" ]
*/
appliesTo: string[]
/**
@@ -57,6 +57,9 @@ export function getDefaultServerMeta(id: string, version: string, options?: Serv
export interface ServerMeta {
+ /**
+ * Server metadata to be forwarded to the distribution file.
+ */
meta: {
version: Server['version']
name: Server['name']
@@ -67,14 +70,30 @@ export interface ServerMeta {
autoconnect: Server['autoconnect']
}
+ /**
+ * Properties related to Forge.
+ */
forge?: {
+ /**
+ * The forge version. This does NOT include the minecraft version.
+ * Ex. 14.23.5.2854
+ */
version: string
}
+ /**
+ * Properties related to liteloader.
+ */
liteloader?: {
+ /**
+ * The liteloader version.
+ */
version: string
}
+ /**
+ * A list of option objects defining patterns for untracked files.
+ */
untrackedFiles?: UntrackedFilesOption[]
}
diff --git a/src/structure/spec_model/Distribution.struct.ts b/src/structure/spec_model/Distribution.struct.ts
index 23296ae..2b62ead 100644
--- a/src/structure/spec_model/Distribution.struct.ts
+++ b/src/structure/spec_model/Distribution.struct.ts
@@ -1,9 +1,13 @@
-import { mkdirs, writeFile, readFile } from 'fs-extra'
+import { mkdirs, writeFile, readFile, pathExists } from 'fs-extra'
import { Distribution } from 'helios-distribution-types'
import { SpecModelStructure } from './SpecModelStructure'
import { ServerStructure } from './Server.struct'
import { join, resolve } from 'path'
import { DistroMeta, getDefaultDistroMeta } from '../../model/nebula/distrometa'
+import { addSchemaToObject, SchemaTypes } from '../../util/SchemaUtil'
+import { LoggerUtil } from '../../util/LoggerUtil'
+
+const logger = LoggerUtil.getLogger('DistributionStructure')
export class DistributionStructure implements SpecModelStructure {
@@ -24,8 +28,18 @@ export class DistributionStructure implements SpecModelStructure {
await mkdirs(this.absoluteRoot)
await mkdirs(this.metaPath)
- const distroMeta: DistroMeta = getDefaultDistroMeta()
- await writeFile(resolve(this.metaPath, this.DISTRO_META_FILE), JSON.stringify(distroMeta, null, 2))
+ const distroMetaFile = resolve(this.metaPath, this.DISTRO_META_FILE)
+ if(await pathExists(distroMetaFile)) {
+ logger.warn(`Distro Meta file already exists at ${distroMetaFile}!`)
+ logger.warn('If you wish to regenerate this file, you must delete the existing one!')
+ } else {
+ const distroMeta: DistroMeta = addSchemaToObject(
+ getDefaultDistroMeta(),
+ SchemaTypes.DistroMetaSchema,
+ this.absoluteRoot
+ )
+ await writeFile(distroMetaFile, JSON.stringify(distroMeta, null, 2))
+ }
await this.serverStruct.init()
}
diff --git a/src/structure/spec_model/Server.struct.ts b/src/structure/spec_model/Server.struct.ts
index 8cd00d2..1cf7426 100644
--- a/src/structure/spec_model/Server.struct.ts
+++ b/src/structure/spec_model/Server.struct.ts
@@ -9,6 +9,7 @@ import { MiscFileStructure } from './module/File.struct'
import { LiteModStructure } from './module/LiteMod.struct'
import { LibraryStructure } from './module/Library.struct'
import { MinecraftVersion } from '../../util/MinecraftVersion'
+import { addSchemaToObject, SchemaTypes } from '../../util/SchemaUtil'
export class ServerStructure extends BaseModelStructure {
@@ -73,7 +74,11 @@ export class ServerStructure extends BaseModelStructure {
serverMetaOpts.liteloaderVersion = options.liteloaderVersion
}
- const serverMeta: ServerMeta = getDefaultServerMeta(id, minecraftVersion.toString(), serverMetaOpts)
+ const serverMeta: ServerMeta = addSchemaToObject(
+ getDefaultServerMeta(id, minecraftVersion.toString(), serverMetaOpts),
+ SchemaTypes.ServerMetaSchema,
+ this.absoluteRoot
+ )
await writeFile(resolvePath(absoluteServerRoot, this.SERVER_META_FILE), JSON.stringify(serverMeta, null, 2))
const libS = new LibraryStructure(absoluteServerRoot, relativeServerRoot, this.baseUrl, minecraftVersion, [])
diff --git a/src/util/SchemaUtil.ts b/src/util/SchemaUtil.ts
new file mode 100644
index 0000000..f38ab0f
--- /dev/null
+++ b/src/util/SchemaUtil.ts
@@ -0,0 +1,73 @@
+import { mkdirs, pathExists, remove, writeFile } from 'fs-extra'
+import { join, resolve } from 'path'
+import { createGenerator } from 'ts-json-schema-generator'
+import { URL } from 'url'
+import { DistroMeta } from '../model/nebula/distrometa'
+import { ServerMeta } from '../model/nebula/servermeta'
+import { LoggerUtil } from './LoggerUtil'
+
+const logger = LoggerUtil.getLogger('SchemaUtil')
+
+interface SchemaType {
+ /**
+ * URL to the JSON schema for this type of file.
+ * This is used by editors to validate and annotate the data.
+ */
+ $schema?: string
+}
+
+export type DistroMetaSchema = DistroMeta & SchemaType
+export type ServerMetaSchema = ServerMeta & SchemaType
+
+export enum SchemaTypes {
+ DistroMetaSchema = 'DistroMetaSchema',
+ ServerMetaSchema = 'ServerMetaSchema'
+}
+
+function getSchemaFileName(typeName: string) {
+ return `${typeName}.schema.json`
+}
+
+function getSchemaDirectory(absoluteRoot: string): string {
+ return resolve(absoluteRoot, 'schemas')
+}
+
+function getSchemaLocation(typeName: string, absoluteRoot: string): string {
+ return resolve(getSchemaDirectory(absoluteRoot), getSchemaFileName(typeName))
+}
+
+export function addSchemaToObject(obj: T, typeName: string, absoluteRoot: string): T {
+ return {
+ $schema: new URL(`file:${getSchemaLocation(typeName, absoluteRoot)}`).href,
+ ...obj
+ }
+}
+
+export async function generateSchemas(absoluteRoot: string): Promise {
+
+ const selfPath = __filename.replace('dist', 'src').replace('.js', '.ts')
+
+ const schemaDir = getSchemaDirectory(absoluteRoot)
+ if(await pathExists(schemaDir)) {
+ await remove(schemaDir)
+ }
+ await mkdirs(schemaDir)
+
+ for(const typeName of Object.values(SchemaTypes)) {
+
+ logger.info(`Generating schema for ${typeName}`)
+
+ const schema = createGenerator({
+ tsconfig: join(__dirname, '..', '..', 'tsconfig.json'),
+ path: selfPath,
+ type: typeName
+ }).createSchema(typeName)
+
+ const schemaString = JSON.stringify(schema)
+ const schemaLoc = getSchemaLocation(typeName, absoluteRoot)
+ await writeFile(schemaLoc, schemaString)
+
+ logger.info(`Schema for ${typeName} saved to ${schemaLoc}`)
+ }
+
+}
\ No newline at end of file