From 2933e0100765d7785f87472e113c077112a46e9a Mon Sep 17 00:00:00 2001 From: leoweyr Date: Fri, 24 Jan 2025 15:57:15 +0800 Subject: [PATCH] feat(cli): add support for manifest and packaging of Legacy Script Engine plugins --- README.md | 24 ++++++++ package.json | 19 +++++-- src/cli/CliLogger.ts | 41 ++++++++++++++ src/cli/index.ts | 55 +++++++++++++++++++ src/nodejs/NodeJsConfiguration.ts | 31 +++++++++-- .../NodeJsConfigurationFileNotFoundError.ts | 25 +++++++++ src/nodejs/NodeJsConfigurationMissingError.ts | 27 +++++++++ src/nodejs/TypeScriptConfiguration.ts | 14 ++++- ...ypeScriptConfigurationFileNotFoundError.ts | 25 +++++++++ .../TypeScriptConfigurationMissingError.ts | 27 +++++++++ .../TypeScriptConfigurationParseError.ts | 34 ++++++++++++ src/packager/Manifest.ts | 29 ++++++++-- .../ManifestConfigurationMissingError.ts | 27 +++++++++ src/packager/ManifestFileNotFoundError.ts | 25 +++++++++ src/packager/Packager.ts | 4 +- src/project/Project.ts | 2 + src/project/TypeScriptProject.ts | 6 +- 17 files changed, 397 insertions(+), 18 deletions(-) create mode 100644 src/cli/CliLogger.ts create mode 100644 src/cli/index.ts create mode 100644 src/nodejs/NodeJsConfigurationFileNotFoundError.ts create mode 100644 src/nodejs/NodeJsConfigurationMissingError.ts create mode 100644 src/nodejs/TypeScriptConfigurationFileNotFoundError.ts create mode 100644 src/nodejs/TypeScriptConfigurationMissingError.ts create mode 100644 src/nodejs/TypeScriptConfigurationParseError.ts create mode 100644 src/packager/ManifestConfigurationMissingError.ts create mode 100644 src/packager/ManifestFileNotFoundError.ts diff --git a/README.md b/README.md index 4ec2a24..2a0ed74 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,27 @@ # Legacy Script Engine Scaffold A utility for assisting in the development of Legacy Script Engine plugins, supporting a native development experience on the Node.js platform. + +> Only TypeScript projects are supported at the moment. + +## 📦 Prepare + +It is a non-intrusive tool, meaning it does not require any mandatory files to be kept in your project. However, it is recommended to add it as a development dependency to your environment for convenient usage: + +```bash +npm install legacy-script-engine-scaffold --save-dev +``` + +## 🚀 Usage + +Generate manifest.json for the Legacy Script Engine plugin: + +```bash +npx lses manifest +``` + +Package the Legacy Script Engine plugin: + +```bash +npx lses pack +``` diff --git a/package.json b/package.json index 97abefc..aba4f72 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,20 @@ { "name": "legacy-script-engine-scaffold", - "version": "0.0.0", - "description": "A utility for assisting in the development of Legacy Script Engine plugins, supporting a native development experience on the Node.js platform.", + "version": "0.1.0", + "description": "A utility for assisting in the development of Legacy Script Engine plugins.", "bugs": "https://github.com/leoweyr/LSEScaffold/issues", + "bin": { + "lses": "dist/cli/index.js" + }, + "files": [ + "dist" + ], "scripts": { "clean": "tsc --build --clean", - "build": "npm run clean & tsc" + "compile": "tsc", + "build": "npm run clean && npm run compile", + "package": "npm run build && npm pack", + "deploy": "npm run package && npm publish" }, "keywords": [ "levilamina", @@ -13,6 +22,7 @@ "bedrock-dedicated-server", "utility", "scaffold", + "cli", "npx" ], "author": "leoweyr ", @@ -23,7 +33,8 @@ }, "dependencies": { "typescript": "^5.7.3", - "archiver": "^7.0.1" + "archiver": "^7.0.1", + "commander": "^13.1.0" }, "repository": { "type": "git", diff --git a/src/cli/CliLogger.ts b/src/cli/CliLogger.ts new file mode 100644 index 0000000..5055f53 --- /dev/null +++ b/src/cli/CliLogger.ts @@ -0,0 +1,41 @@ +import { CliLoggableError } from "./CliLoggableError"; + + +export class CliLogger { + private static TOOL_NAME: string = "legacy-script-engine-scaffold"; + + private readonly methodName: string; + + public constructor(methodName: string) { + this.methodName = methodName; + } + + public success(msg: string): void { + console.log( + `✅ ${CliLogger.TOOL_NAME}::${this.methodName}: ${msg}` + ) + } + + public error(error: CliLoggableError): void { + let suggestionString: string = ""; + + if (error.getSuggestion().length === 1) { + suggestionString += `Suggestion: ${error.getSuggestion()[0]}`; + } else { + suggestionString += "Suggestions:\n"; + + let solutionIndex: number = 1; + + for (const solution of error.getSuggestion()) { + suggestionString += ` ${solutionIndex}. ${solution}\n`; + solutionIndex++; + } + + suggestionString = suggestionString.slice(0, -2); // Remove the last newline. + } + + console.error( + `❌ ${CliLogger.TOOL_NAME}::${this.methodName}: ${error.constructor.name} - ${error.getMessage()}\n ${suggestionString}` + ); + } +} diff --git a/src/cli/index.ts b/src/cli/index.ts new file mode 100644 index 0000000..bdfac58 --- /dev/null +++ b/src/cli/index.ts @@ -0,0 +1,55 @@ +#!/usr/bin/env node + + +import { program } from "commander"; + +import { CliLogger } from "./CliLogger"; +import { TypeScriptProject } from "../project/TypeScriptProject"; +import { CliLoggableError } from "./CliLoggableError"; +import { Packager } from "../packager/Packager"; + + +program + .name("lses") + .version("0.1.0") + .description("A utility for assisting in the development of Legacy Script Engine plugins."); + +program + .command("manifest") + .description("generate manifest.json for the Legacy Script Engine plugin") + .action((): void => { + const logger: CliLogger = new CliLogger("manifest"); + + try { + const project: TypeScriptProject = TypeScriptProject.getInstance(); + + const successMessage: string = project.getManifest().generate(); + logger.success(successMessage); + } catch (error) { + logger.error(error as CliLoggableError); + } + }); + +program + .command("pack") + .description("package the Legacy Script Engine plugin") + .action(async (): Promise => { + const logger = new CliLogger("pack"); + + try { + const project: TypeScriptProject = TypeScriptProject.getInstance(); + const packager: Packager = new Packager(project); + + const successMessage: string = await packager.package(); + logger.success(successMessage); + } catch (error) { + logger.error(error as CliLoggableError); + } + }); + +program.on("command:*", (): void => { + console.error(`Error: Invalid command lses ${program.args.join(" ")}`); + program.help(); +}); + +program.parse(process.argv); diff --git a/src/nodejs/NodeJsConfiguration.ts b/src/nodejs/NodeJsConfiguration.ts index 347bcbf..3899a84 100644 --- a/src/nodejs/NodeJsConfiguration.ts +++ b/src/nodejs/NodeJsConfiguration.ts @@ -2,13 +2,21 @@ import * as Path from "path"; import * as File from "fs"; import { Project } from "../project/Project"; +import { NodeJsConfigurationFileNotFoundError } from "./NodeJsConfigurationFileNotFoundError"; +import { NodeJsConfigurationMissingError } from "./NodeJsConfigurationMissingError"; export class NodeJsConfiguration { private readonly filePath: string; public constructor(project: Project) { - this.filePath = Path.join(project.getPath(), "package.json"); + const projectPath: string = project.getPath(); + + this.filePath = Path.join(projectPath, "package.json"); + + if (!File.existsSync(this.filePath)) { + throw new NodeJsConfigurationFileNotFoundError(projectPath); + } } public getPath(): string { @@ -17,19 +25,34 @@ export class NodeJsConfiguration { public getName(): string { const configuration: any = JSON.parse(File.readFileSync(this.filePath, "utf-8")); + const name: string = configuration.name; + + if (!name) { + throw new NodeJsConfigurationMissingError(this.filePath, "name"); + } - return configuration.name; + return name; } public getVersion(): string { const configuration: any = JSON.parse(File.readFileSync(this.filePath, "utf-8")); + const version: string = configuration.version; - return configuration.version; + if (!version) { + throw new NodeJsConfigurationMissingError(this.filePath, "version"); + } + + return version; } public getMainEntry(): string { const configuration: any = JSON.parse(File.readFileSync(this.filePath, "utf-8")); + const mainEntry: string = configuration.main; + + if (!mainEntry) { + throw new NodeJsConfigurationMissingError(this.filePath, "main"); + } - return configuration.main; + return mainEntry; } } diff --git a/src/nodejs/NodeJsConfigurationFileNotFoundError.ts b/src/nodejs/NodeJsConfigurationFileNotFoundError.ts new file mode 100644 index 0000000..45c7e79 --- /dev/null +++ b/src/nodejs/NodeJsConfigurationFileNotFoundError.ts @@ -0,0 +1,25 @@ +import { CliLoggableError } from "../cli/CliLoggableError"; + + +export class NodeJsConfigurationFileNotFoundError extends Error implements CliLoggableError { + private readonly msg: string; + + public constructor(fileDirectory: string) { + const message: string = `Could not find package.json in ${fileDirectory}.`; + + super(message); + + this.msg = message; + } + + public getMessage(): string { + return this.msg; + } + + public getSuggestion(): Array { + const suggestion: Array = new Array(); + suggestion.push("Try `npm init` to initialize the project."); + + return suggestion; + } +} diff --git a/src/nodejs/NodeJsConfigurationMissingError.ts b/src/nodejs/NodeJsConfigurationMissingError.ts new file mode 100644 index 0000000..c6faf17 --- /dev/null +++ b/src/nodejs/NodeJsConfigurationMissingError.ts @@ -0,0 +1,27 @@ +import { CliLoggableError } from "../cli/CliLoggableError"; + + +export class NodeJsConfigurationMissingError extends Error implements CliLoggableError { + private readonly msg: string; + private readonly missingProperty: string; + + public constructor(filePath: string, missingProperty: string) { + const message: string = `${filePath} is missing the required property \`${missingProperty}\`.`; + + super(message); + + this.msg = message; + this.missingProperty = missingProperty; + } + + public getMessage(): string { + return this.msg; + } + + public getSuggestion(): Array { + const suggestion: Array = new Array(); + suggestion.push(`Try checking if package.json includes the \`${this.missingProperty}\` configuration.`); + + return suggestion; + } +} diff --git a/src/nodejs/TypeScriptConfiguration.ts b/src/nodejs/TypeScriptConfiguration.ts index d7f15fc..2aca32d 100644 --- a/src/nodejs/TypeScriptConfiguration.ts +++ b/src/nodejs/TypeScriptConfiguration.ts @@ -1,8 +1,12 @@ import * as Path from "path"; +import * as File from "fs"; import ts, { ParsedCommandLine, Program } from "typescript"; import { Project } from "../project/Project"; +import { TypeScriptConfigurationParseError } from "./TypeScriptConfigurationParseError"; +import { TypeScriptConfigurationFileNotFoundError } from "./TypeScriptConfigurationFileNotFoundError"; +import { TypeScriptConfigurationMissingError } from "./TypeScriptConfigurationMissingError"; export class TypeScriptConfiguration { @@ -10,7 +14,7 @@ export class TypeScriptConfiguration { const file: any = ts.readConfigFile(filePath, ts.sys.readFile); if (file.error) { - throw new Error("Error reading tsconfig.json."); + throw new TypeScriptConfigurationParseError(filePath); } const parsedCommandLine: ParsedCommandLine = ts.parseJsonConfigFileContent( @@ -20,7 +24,7 @@ export class TypeScriptConfiguration { ); if (parsedCommandLine.errors.length > 0) { - throw new Error(`Error parsing tsconfig.json: ${JSON.stringify(parsedCommandLine.errors)}.`); + throw new TypeScriptConfigurationParseError(filePath, parsedCommandLine.errors); } return parsedCommandLine; @@ -30,6 +34,10 @@ export class TypeScriptConfiguration { public constructor(project: Project) { this.filePath = Path.join(project.getPath(), "tsconfig.json"); + + if (!File.existsSync(this.filePath)) { + throw new TypeScriptConfigurationFileNotFoundError(project.getPath()); + } } public getEmittedDirectory(): string { @@ -37,7 +45,7 @@ export class TypeScriptConfiguration { const emittedDirectory: string | undefined = parsedCommandLine.options.outDir; if (!emittedDirectory) { - throw new Error("No `outDir` configuration in tsconfig.json."); + throw new TypeScriptConfigurationMissingError(this.filePath, "outDir"); } return emittedDirectory; diff --git a/src/nodejs/TypeScriptConfigurationFileNotFoundError.ts b/src/nodejs/TypeScriptConfigurationFileNotFoundError.ts new file mode 100644 index 0000000..4fb0d48 --- /dev/null +++ b/src/nodejs/TypeScriptConfigurationFileNotFoundError.ts @@ -0,0 +1,25 @@ +import { CliLoggableError } from "../cli/CliLoggableError"; + + +export class TypeScriptConfigurationFileNotFoundError extends Error implements CliLoggableError { + private readonly msg: string; + + public constructor(fileDirectory: string) { + const message: string = `Could not find tsconfig.json in ${fileDirectory}.`; + + super(message); + + this.msg = message; + } + + public getMessage(): string { + return this.msg; + } + + public getSuggestion(): Array { + const suggestion: Array = new Array(); + suggestion.push("Try `npx tsc --init` to initialize the TypeScript project."); + + return suggestion; + } +} diff --git a/src/nodejs/TypeScriptConfigurationMissingError.ts b/src/nodejs/TypeScriptConfigurationMissingError.ts new file mode 100644 index 0000000..f8b12cf --- /dev/null +++ b/src/nodejs/TypeScriptConfigurationMissingError.ts @@ -0,0 +1,27 @@ +import { CliLoggableError } from "../cli/CliLoggableError"; + + +export class TypeScriptConfigurationMissingError extends Error implements CliLoggableError { + private readonly msg: string; + private readonly missingProperty: string; + + public constructor(filePath: string, missingProperty: string) { + const message: string = `${filePath} is missing the required property \`${missingProperty}\`.`; + + super(message); + + this.msg = message; + this.missingProperty = missingProperty; + } + + public getMessage(): string { + return this.msg; + } + + public getSuggestion(): Array { + const suggestion: Array = new Array(); + suggestion.push(`Try checking if tsconfig.json includes the \`${this.missingProperty}\` configuration.`); + + return suggestion; + } +} diff --git a/src/nodejs/TypeScriptConfigurationParseError.ts b/src/nodejs/TypeScriptConfigurationParseError.ts new file mode 100644 index 0000000..628c2dc --- /dev/null +++ b/src/nodejs/TypeScriptConfigurationParseError.ts @@ -0,0 +1,34 @@ +import { CliLoggableError } from "../cli/CliLoggableError"; +import { Diagnostic } from "typescript"; + + +export class TypeScriptConfigurationParseError extends Error implements CliLoggableError { + private readonly msg: string; + private readonly diagnostics: Array | null; + + public constructor(filePath: string, diagnostics: Array | null = null) { + const message: string = `Failed to parse ${filePath}.`; + + super(message); + + this.msg = message; + this.diagnostics = diagnostics; + } + + public getMessage(): string { + return this.msg; + } + + public getSuggestion(): Array { + const suggestion: Array = new Array(); + suggestion.push("Try checking the tsconfig.json."); + + if (this.diagnostics !== null) { + suggestion.push( + `Try to resolve the issue based on the diagnostic information from the TypeScript compiler: ${JSON.stringify(this.diagnostics)}` + ); + } + + return suggestion; + } +} diff --git a/src/packager/Manifest.ts b/src/packager/Manifest.ts index f35cd77..dc742b3 100644 --- a/src/packager/Manifest.ts +++ b/src/packager/Manifest.ts @@ -3,6 +3,8 @@ import * as File from "fs"; import { Project } from "../project/Project"; import { NodeJsConfiguration } from "../nodejs/NodeJsConfiguration"; +import { ManifestFileNotFoundError } from "./ManifestFileNotFoundError"; +import { ManifestConfigurationMissingError } from "./ManifestConfigurationMissingError"; export class Manifest { @@ -16,11 +18,26 @@ export class Manifest { return Path.join(this.project.getBuiltPath(), "manifest.json"); } - public generate(): void { - const packageConfiguration: NodeJsConfiguration = this.project.getNodeJsConfiguration(); - const name: string = packageConfiguration.getName(); - const version: string = packageConfiguration.getVersion(); - const entry: string = packageConfiguration.getMainEntry(); + public getName(): string { + if (!File.existsSync(this.getPath())) { + throw new ManifestFileNotFoundError(this.project.getBuiltPath()); + } + + const manifest: any = JSON.parse(File.readFileSync(this.getPath(), "utf-8")); + const name: string = manifest.name; + + if (!name) { + throw new ManifestConfigurationMissingError(this.getPath(), "name"); + } + + return name; + } + + public generate(): string { + const nodeJsConfiguration: NodeJsConfiguration = this.project.getNodeJsConfiguration(); + const name: string = nodeJsConfiguration.getName(); + const version: string = nodeJsConfiguration.getVersion(); + const entry: string = nodeJsConfiguration.getMainEntry(); const manifest = { "name": name, @@ -31,6 +48,8 @@ export class Manifest { }; File.writeFileSync(this.getPath(), JSON.stringify(manifest, null, 2), "utf-8"); + + return `The manifest has been generated at ${this.getPath()}.`; } public static isValid(filePath: string): boolean { diff --git a/src/packager/ManifestConfigurationMissingError.ts b/src/packager/ManifestConfigurationMissingError.ts new file mode 100644 index 0000000..0f5096d --- /dev/null +++ b/src/packager/ManifestConfigurationMissingError.ts @@ -0,0 +1,27 @@ +import { CliLoggableError } from "../cli/CliLoggableError"; + + +export class ManifestConfigurationMissingError extends Error implements CliLoggableError { + private readonly msg: string; + private readonly missingProperty: string; + + public constructor(filePath: string, missingProperty: string) { + const message: string = `${filePath} is missing the required property \`${missingProperty}\`.`; + + super(message); + + this.msg = message; + this.missingProperty = missingProperty; + } + + public getMessage(): string { + return this.msg; + } + + public getSuggestion(): Array { + const suggestion: Array = new Array(); + suggestion.push(`Try checking if manifest.json includes the \`${this.missingProperty}\` configuration.`); + + return suggestion; + } +} diff --git a/src/packager/ManifestFileNotFoundError.ts b/src/packager/ManifestFileNotFoundError.ts new file mode 100644 index 0000000..8462687 --- /dev/null +++ b/src/packager/ManifestFileNotFoundError.ts @@ -0,0 +1,25 @@ +import { CliLoggableError } from "../cli/CliLoggableError"; + + +export class ManifestFileNotFoundError extends Error implements CliLoggableError{ + private readonly msg: string; + + public constructor(fileDirectory: string) { + const message: string = `Could not find manifest.json in ${fileDirectory}.`; + + super(message); + + this.msg = message; + } + + public getMessage(): string { + return this.msg; + } + + public getSuggestion(): Array { + const suggestion: Array = new Array(); + suggestion.push("Try `npx lses manifest` to manifest the project."); + + return suggestion; + } +} diff --git a/src/packager/Packager.ts b/src/packager/Packager.ts index 101a539..01d7a5c 100644 --- a/src/packager/Packager.ts +++ b/src/packager/Packager.ts @@ -15,7 +15,7 @@ export class Packager { this.project = project; } - public async package(): Promise { + public async package(): Promise { if (!this.project.isBuilt()) { throw new ProjectNotBuiltError(); } @@ -42,5 +42,7 @@ export class Packager { archive.pipe(outputStream); archive.directory(this.project.getBuiltPath(), false); await archive.finalize(); + + return `The package has been generated at ${outputStream.path}.`; } } diff --git a/src/project/Project.ts b/src/project/Project.ts index d8a2e57..933f5c2 100644 --- a/src/project/Project.ts +++ b/src/project/Project.ts @@ -1,4 +1,5 @@ import { NodeJsConfiguration } from "../nodejs/NodeJsConfiguration"; +import { Manifest } from "../packager/Manifest"; export interface Project { @@ -6,6 +7,7 @@ export interface Project { getPath(): string; getBuiltPath(): string; getNodeJsConfiguration(): NodeJsConfiguration; + getManifest(): Manifest; isBuilt(): boolean; isManifest(): boolean; } diff --git a/src/project/TypeScriptProject.ts b/src/project/TypeScriptProject.ts index 4b5c700..8341685 100644 --- a/src/project/TypeScriptProject.ts +++ b/src/project/TypeScriptProject.ts @@ -56,7 +56,7 @@ export class TypeScriptProject implements Project { } public getName(): string { - return this.nodeJsConfiguration.getName(); + return this.manifest.getName(); } public getPath(): string { @@ -71,6 +71,10 @@ export class TypeScriptProject implements Project { return this.nodeJsConfiguration; } + public getManifest(): Manifest { + return this.manifest; + } + public isBuilt(): boolean { const emittedDirectoryStructure: Array = TypeScriptProject.getDirectoryStructure( this.typeScriptConfiguration.getEmittedDirectory()