@jpp-toolkit/plugin-build-fivem 0.0.13 → 0.0.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.mts CHANGED
@@ -6,7 +6,7 @@ declare class FivemBuildCommand extends Command {
6
6
  static summary: string;
7
7
  static flags: {
8
8
  watch: _oclif_core_interfaces0.BooleanFlag<boolean>;
9
- mode: _oclif_core_interfaces0.OptionFlag<string | undefined, _oclif_core_interfaces0.CustomOptions>;
9
+ env: _oclif_core_interfaces0.OptionFlag<string | undefined, _oclif_core_interfaces0.CustomOptions>;
10
10
  autoReload: _oclif_core_interfaces0.BooleanFlag<boolean>;
11
11
  server: _oclif_core_interfaces0.OptionFlag<string, _oclif_core_interfaces0.CustomOptions>;
12
12
  password: _oclif_core_interfaces0.OptionFlag<string | undefined, _oclif_core_interfaces0.CustomOptions>;
@@ -17,9 +17,7 @@ declare class FivemBuildCommand extends Command {
17
17
  command: string;
18
18
  }[];
19
19
  run(): Promise<void>;
20
- private _parseServerAddress;
21
- private _compilerCallback;
22
- private _enableAutoReload;
20
+ private _parseOptions;
23
21
  }
24
22
  //#endregion
25
23
  //#region src/index.d.ts
package/dist/index.mjs CHANGED
@@ -1,11 +1,54 @@
1
1
  import path from "node:path";
2
2
  import { Command } from "@jpp-toolkit/core";
3
- import { FivemRcon } from "@jpp-toolkit/rcon";
4
3
  import { createFivemRspackConfig } from "@jpp-toolkit/rspack-config";
5
4
  import { getErrMsg } from "@jpp-toolkit/utils";
6
5
  import { Flags } from "@oclif/core";
7
6
  import { rspack } from "@rspack/core";
7
+ import dgram from "node:dgram";
8
8
 
9
+ //#region src/fivem-rcon.ts
10
+ function buildCommand(command, password) {
11
+ const buffer = Buffer.alloc(11 + password.length + command.length);
12
+ buffer.writeUInt32LE(4294967295, 0);
13
+ buffer.write("rcon ", 4);
14
+ buffer.write(password, 9, password.length);
15
+ buffer.write(" ", 9 + password.length, 1);
16
+ buffer.write(command, 10 + password.length, command.length);
17
+ buffer.write("\n", 10 + password.length + command.length, 1);
18
+ return buffer;
19
+ }
20
+ async function sendFivemRcon(command, options) {
21
+ const socket = dgram.createSocket("udp4");
22
+ return new Promise((resolve, reject) => {
23
+ let timeoutId;
24
+ const handleError = (err) => {
25
+ if (!err) return;
26
+ clearTimeout(timeoutId);
27
+ const msg = getErrMsg(err);
28
+ reject(/* @__PURE__ */ new Error(`Failed to send command to ${options.host}:${options.port}: ${msg}`));
29
+ };
30
+ const handleMessage = (msg) => {
31
+ clearTimeout(timeoutId);
32
+ const res = msg.toString("ascii").slice(4).trim();
33
+ if (res.includes("Invalid password")) return void handleError("Invalid password");
34
+ resolve(res);
35
+ };
36
+ timeoutId = setTimeout(() => handleError(`Timeout after ${options.timeout}ms`), options.timeout ?? 5e3);
37
+ socket.once("error", handleError);
38
+ socket.once("message", handleMessage);
39
+ const cmd = buildCommand(command, options.password);
40
+ socket.send(cmd, 0, cmd.length, options.port, options.host, handleError);
41
+ }).finally(() => {
42
+ socket.close();
43
+ });
44
+ }
45
+ async function refreshAndEnsureFivemResource(resourceName, options) {
46
+ const res = await sendFivemRcon(`refresh; ensure ${resourceName}`, options);
47
+ if (res.includes("Couldn't find resource")) throw new Error(`Resource "${resourceName}" not found`);
48
+ if (!res.includes("Started resource")) throw new Error(`Failed to start resource "${resourceName}"`);
49
+ }
50
+
51
+ //#endregion
9
52
  //#region src/fivem-build-command.ts
10
53
  var FivemBuildCommand = class FivemBuildCommand extends Command {
11
54
  static summary = "Build the FiveM resource using predefined config.";
@@ -15,9 +58,9 @@ var FivemBuildCommand = class FivemBuildCommand extends Command {
15
58
  description: "Watch files for changes and rebuild automatically.",
16
59
  default: false
17
60
  }),
18
- mode: Flags.string({
19
- char: "m",
20
- description: "Set the build mode (development or production).",
61
+ env: Flags.string({
62
+ char: "e",
63
+ description: "Set the build environment (development or production).",
21
64
  options: ["development", "production"],
22
65
  required: false
23
66
  }),
@@ -53,8 +96,8 @@ var FivemBuildCommand = class FivemBuildCommand extends Command {
53
96
  command: "<%= config.bin %> <%= command.id %> --watch"
54
97
  },
55
98
  {
56
- description: "Build the FiveM resource in production mode.",
57
- command: "<%= config.bin %> <%= command.id %> --mode=production"
99
+ description: "Build the FiveM resource in production environment.",
100
+ command: "<%= config.bin %> <%= command.id %> --env=production"
58
101
  },
59
102
  {
60
103
  description: "Build the FiveM resource and automatically reload it on the server after build.",
@@ -70,60 +113,61 @@ var FivemBuildCommand = class FivemBuildCommand extends Command {
70
113
  }
71
114
  ];
72
115
  async run() {
73
- const { flags } = await this.parse(FivemBuildCommand);
74
- const { watch, autoReload, password } = flags;
75
- const mode = flags.mode ?? (watch ? "development" : "production");
76
- const { host, port } = this._parseServerAddress(flags.server);
77
- const resourceName = flags.resourceName ?? path.basename(process.cwd());
78
- const compiler = rspack(createFivemRspackConfig({ mode }));
79
- if (autoReload) {
80
- if (!password) this.fatalError("RCON password is required for auto-reload. Please provide it using the --password flag.");
81
- this._enableAutoReload(compiler, resourceName, host, port, password);
82
- }
83
- if (flags.watch) {
116
+ const { env, resourceName, rconOptions, watch } = await this._parseOptions();
117
+ const compiler = rspack(createFivemRspackConfig({
118
+ environment: env,
119
+ resourceName
120
+ }));
121
+ const reloadFivemResource = async () => {
122
+ if (!rconOptions) return;
123
+ this.logger.info(`Reloading FiveM resource "${resourceName}"...`);
124
+ try {
125
+ await refreshAndEnsureFivemResource(resourceName, rconOptions);
126
+ this.logger.success(`FiveM resource reloaded successfully.\n`);
127
+ } catch (error) {
128
+ this.logger.error(`Failed to reload FiveM resource: ${getErrMsg(error)}\n`);
129
+ }
130
+ };
131
+ const compilerCallback = (err, stats) => {
132
+ if (err) {
133
+ this.logger.error(getErrMsg(err));
134
+ if ("details" in err) this.logger.error(err.details);
135
+ if ("stack" in err) this.logger.error(err.stack);
136
+ return;
137
+ }
138
+ if (stats) this.logger.log(stats.toString(true), "\n");
139
+ reloadFivemResource();
140
+ };
141
+ if (watch) {
84
142
  this.logger.info(`Building FiveM resource in watch mode...\n`);
85
- compiler.watch({}, (err, stats) => this._compilerCallback(err, stats));
143
+ compiler.watch({}, compilerCallback);
86
144
  } else {
87
145
  this.logger.info(`Building FiveM resource...\n`);
88
- compiler.run((err, stats) => this._compilerCallback(err, stats));
146
+ compiler.run(compilerCallback);
89
147
  }
90
148
  }
91
- _parseServerAddress(address) {
92
- const { host, port } = /^(?<host>[^:]+):(?<port>\d+)$/u.exec(address)?.groups ?? {};
93
- if (!host || !port) throw new Error(`Invalid server address format: ${address}. Expected format is "ip:port".`);
149
+ async _parseOptions() {
150
+ const { flags } = await this.parse(FivemBuildCommand);
151
+ const env = flags.env ?? (flags.watch ? "development" : "production");
152
+ const resourceName = flags.resourceName ?? path.basename(process.cwd());
153
+ let rconOptions;
154
+ if (flags.autoReload) {
155
+ if (!flags.password) throw new Error("RCON password is required for auto-reload. Please provide it using the --password flag.");
156
+ const { host, port } = /^(?<host>[^:]+):(?<port>\d+)$/u.exec(flags.server)?.groups ?? {};
157
+ if (!host || !port) throw new Error(`Invalid server address format: ${flags.server}. Expected format is "ip:port".`);
158
+ rconOptions = {
159
+ host,
160
+ port: parseInt(port),
161
+ password: flags.password
162
+ };
163
+ }
94
164
  return {
95
- host,
96
- port: parseInt(port)
165
+ env,
166
+ resourceName,
167
+ rconOptions,
168
+ watch: flags.watch
97
169
  };
98
170
  }
99
- _compilerCallback(err, stats) {
100
- if (err) {
101
- this.logger.error(getErrMsg(err));
102
- if ("details" in err) this.logger.error(err.details);
103
- if ("stack" in err) this.logger.error(err.stack);
104
- return;
105
- }
106
- if (stats) this.logger.log(stats.toString(), "\n");
107
- }
108
- _enableAutoReload(compiler, resourceName, host, port, password) {
109
- compiler.hooks.done.tapPromise("FivemAutoReloadPlugin", async (stats) => {
110
- if (stats.hasErrors()) {
111
- this.logger.warning("Build failed. Skipping FiveM resource reload.\n");
112
- return;
113
- }
114
- this.logger.info(`Reloading FiveM resource "${resourceName}"...`);
115
- try {
116
- await new FivemRcon({
117
- host,
118
- port,
119
- password
120
- }).refreshAndEnsureResource(resourceName);
121
- this.logger.success(`FiveM resource reloaded successfully.\n`);
122
- } catch (error) {
123
- this.logger.error(`Failed to reload FiveM resource: ${getErrMsg(error)}\n`);
124
- }
125
- });
126
- }
127
171
  };
128
172
 
129
173
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../src/fivem-build-command.ts","../src/index.ts"],"sourcesContent":["import path from 'node:path';\n\nimport { Command } from '@jpp-toolkit/core';\nimport { FivemRcon } from '@jpp-toolkit/rcon';\nimport { createFivemRspackConfig } from '@jpp-toolkit/rspack-config';\nimport { getErrMsg } from '@jpp-toolkit/utils';\nimport { Flags } from '@oclif/core';\nimport type { Compiler, Stats } from '@rspack/core';\nimport { rspack } from '@rspack/core';\n\ntype Mode = 'development' | 'production';\ntype ServerAddress = { readonly host: string; readonly port: number };\n\nexport class FivemBuildCommand extends Command {\n static override summary = 'Build the FiveM resource using predefined config.';\n\n static override flags = {\n watch: Flags.boolean({\n char: 'w',\n description: 'Watch files for changes and rebuild automatically.',\n default: false,\n }),\n mode: Flags.string({\n char: 'm',\n description: 'Set the build mode (development or production).',\n options: ['development', 'production'],\n required: false,\n }),\n autoReload: Flags.boolean({\n char: 'r',\n description: 'Automatically reload FiveM resource after build.',\n default: false,\n }),\n server: Flags.string({\n char: 's',\n description: 'Server \"ip:port\" to connect for reloading FiveM resource after build.',\n default: '127.0.0.1:30120',\n required: false,\n }),\n password: Flags.string({\n char: 'p',\n description: 'RCON password for the FiveM server to reload resource after build.',\n required: false,\n }),\n resourceName: Flags.string({\n char: 'n',\n description:\n 'Name of the FiveM resource to reload. If not provided, the name of the folder containing the resource will be used.',\n required: false,\n }),\n };\n\n static override examples = [\n {\n description: 'Build the FiveM resource.',\n command: '<%= config.bin %> <%= command.id %>',\n },\n {\n description: 'Build the FiveM resource in watch mode.',\n command: '<%= config.bin %> <%= command.id %> --watch',\n },\n {\n description: 'Build the FiveM resource in production mode.',\n command: '<%= config.bin %> <%= command.id %> --mode=production',\n },\n {\n description:\n 'Build the FiveM resource and automatically reload it on the server after build.',\n command:\n '<%= config.bin %> <%= command.id %> --auto-reload --password your_rcon_password',\n },\n {\n description:\n 'Build the FiveM resource in watch mode and automatically reload it on the server after each build.',\n command:\n '<%= config.bin %> <%= command.id %> --watch --auto-reload --password your_rcon_password',\n },\n {\n description:\n 'Build the FiveM resource and connect to a specific server for auto-reload.',\n command:\n '<%= config.bin %> <%= command.id %> --auto-reload --server=127.0.0.1:30120 --password your_rcon_password',\n },\n ];\n\n public async run(): Promise<void> {\n const { flags } = await this.parse(FivemBuildCommand);\n\n const { watch, autoReload, password } = flags;\n const mode = (flags.mode ?? (watch ? 'development' : 'production')) as Mode;\n const { host, port } = this._parseServerAddress(flags.server);\n const resourceName = flags.resourceName ?? path.basename(process.cwd());\n\n const config = createFivemRspackConfig({ mode });\n const compiler = rspack(config);\n\n if (autoReload) {\n if (!password) {\n this.fatalError(\n 'RCON password is required for auto-reload. Please provide it using the --password flag.',\n );\n }\n\n this._enableAutoReload(compiler, resourceName, host, port, password);\n }\n\n if (flags.watch) {\n this.logger.info(`Building FiveM resource in watch mode...\\n`);\n compiler.watch({}, (err, stats) => this._compilerCallback(err, stats));\n } else {\n this.logger.info(`Building FiveM resource...\\n`);\n compiler.run((err, stats) => this._compilerCallback(err, stats));\n }\n }\n\n private _parseServerAddress(address: string): ServerAddress {\n const match = /^(?<host>[^:]+):(?<port>\\d+)$/u.exec(address);\n const { host, port } = match?.groups ?? {};\n\n if (!host || !port) {\n throw new Error(\n `Invalid server address format: ${address}. Expected format is \"ip:port\".`,\n );\n }\n\n return { host, port: parseInt(port) };\n }\n\n private _compilerCallback(err: Error | null, stats: Stats | undefined): void {\n if (err) {\n this.logger.error(getErrMsg(err));\n if ('details' in err) this.logger.error(err.details as string);\n if ('stack' in err) this.logger.error(err.stack);\n return;\n }\n\n if (stats) this.logger.log(stats.toString(), '\\n');\n }\n\n private _enableAutoReload(\n compiler: Compiler,\n resourceName: string,\n host: string,\n port: number,\n password: string,\n ): void {\n compiler.hooks.done.tapPromise('FivemAutoReloadPlugin', async (stats) => {\n if (stats.hasErrors()) {\n this.logger.warning('Build failed. Skipping FiveM resource reload.\\n');\n return;\n }\n\n this.logger.info(`Reloading FiveM resource \"${resourceName}\"...`);\n try {\n const rcon = new FivemRcon({ host, port, password });\n await rcon.refreshAndEnsureResource(resourceName);\n this.logger.success(`FiveM resource reloaded successfully.\\n`);\n } catch (error) {\n this.logger.error(`Failed to reload FiveM resource: ${getErrMsg(error)}\\n`);\n }\n });\n }\n}\n","import { FivemBuildCommand } from './fivem-build-command';\n\nexport const commands = {\n 'build:fivem': FivemBuildCommand,\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.mjs","names":["timeoutId: NodeJS.Timeout","rconOptions: RconOptions | undefined"],"sources":["../src/fivem-rcon.ts","../src/fivem-build-command.ts","../src/index.ts"],"sourcesContent":["import dgram from 'node:dgram';\n\nimport { getErrMsg } from '@jpp-toolkit/utils';\n\nexport type RconOptions = {\n readonly host: string;\n readonly port: number;\n readonly password: string;\n readonly timeout?: number | undefined;\n};\n\nfunction buildCommand(command: string, password: string): Buffer {\n const buffer = Buffer.alloc(11 + password.length + command.length);\n buffer.writeUInt32LE(0xffffffff, 0);\n buffer.write('rcon ', 4);\n buffer.write(password, 9, password.length);\n buffer.write(' ', 9 + password.length, 1);\n buffer.write(command, 10 + password.length, command.length);\n buffer.write('\\n', 10 + password.length + command.length, 1);\n return buffer;\n}\n\nexport async function sendFivemRcon(command: string, options: RconOptions): Promise<string> {\n const socket = dgram.createSocket('udp4');\n\n return new Promise<string>((resolve, reject) => {\n let timeoutId: NodeJS.Timeout;\n\n const handleError = (err?: Error | string | null) => {\n if (!err) return;\n clearTimeout(timeoutId);\n const msg = getErrMsg(err);\n reject(new Error(`Failed to send command to ${options.host}:${options.port}: ${msg}`));\n };\n\n const handleMessage = (msg: Buffer) => {\n clearTimeout(timeoutId);\n const res = msg.toString('ascii').slice(4).trim();\n if (res.includes('Invalid password')) return void handleError('Invalid password');\n resolve(res);\n };\n\n timeoutId = setTimeout(\n () => handleError(`Timeout after ${options.timeout}ms`),\n options.timeout ?? 5000,\n );\n\n socket.once('error', handleError);\n socket.once('message', handleMessage);\n\n const cmd = buildCommand(command, options.password);\n socket.send(cmd, 0, cmd.length, options.port, options.host, handleError);\n }).finally(() => {\n socket.close();\n });\n}\n\nexport async function refreshAndEnsureFivemResource(\n resourceName: string,\n options: RconOptions,\n): Promise<void> {\n const res = await sendFivemRcon(`refresh; ensure ${resourceName}`, options);\n if (res.includes(\"Couldn't find resource\"))\n throw new Error(`Resource \"${resourceName}\" not found`);\n if (!res.includes('Started resource'))\n throw new Error(`Failed to start resource \"${resourceName}\"`);\n}\n","import path from 'node:path';\n\nimport { Command } from '@jpp-toolkit/core';\nimport { createFivemRspackConfig } from '@jpp-toolkit/rspack-config';\nimport { getErrMsg } from '@jpp-toolkit/utils';\nimport { Flags } from '@oclif/core';\nimport type { MultiStats } from '@rspack/core';\nimport { rspack } from '@rspack/core';\n\nimport { refreshAndEnsureFivemResource } from './fivem-rcon';\nimport type { RconOptions } from './fivem-rcon';\n\ntype Environment = 'development' | 'production';\n\ntype FivemBuildCommandOptions = {\n readonly env: Environment;\n readonly resourceName: string;\n readonly rconOptions?: RconOptions | undefined;\n readonly watch: boolean;\n};\n\nexport class FivemBuildCommand extends Command {\n static override summary = 'Build the FiveM resource using predefined config.';\n\n static override flags = {\n watch: Flags.boolean({\n char: 'w',\n description: 'Watch files for changes and rebuild automatically.',\n default: false,\n }),\n env: Flags.string({\n char: 'e',\n description: 'Set the build environment (development or production).',\n options: ['development', 'production'],\n required: false,\n }),\n autoReload: Flags.boolean({\n char: 'r',\n description: 'Automatically reload FiveM resource after build.',\n default: false,\n }),\n server: Flags.string({\n char: 's',\n description: 'Server \"ip:port\" to connect for reloading FiveM resource after build.',\n default: '127.0.0.1:30120',\n required: false,\n }),\n password: Flags.string({\n char: 'p',\n description: 'RCON password for the FiveM server to reload resource after build.',\n required: false,\n }),\n resourceName: Flags.string({\n char: 'n',\n description:\n 'Name of the FiveM resource to reload. If not provided, the name of the folder containing the resource will be used.',\n required: false,\n }),\n };\n\n static override examples = [\n {\n description: 'Build the FiveM resource.',\n command: '<%= config.bin %> <%= command.id %>',\n },\n {\n description: 'Build the FiveM resource in watch mode.',\n command: '<%= config.bin %> <%= command.id %> --watch',\n },\n {\n description: 'Build the FiveM resource in production environment.',\n command: '<%= config.bin %> <%= command.id %> --env=production',\n },\n {\n description:\n 'Build the FiveM resource and automatically reload it on the server after build.',\n command:\n '<%= config.bin %> <%= command.id %> --auto-reload --password your_rcon_password',\n },\n {\n description:\n 'Build the FiveM resource in watch mode and automatically reload it on the server after each build.',\n command:\n '<%= config.bin %> <%= command.id %> --watch --auto-reload --password your_rcon_password',\n },\n {\n description:\n 'Build the FiveM resource and connect to a specific server for auto-reload.',\n command:\n '<%= config.bin %> <%= command.id %> --auto-reload --server=127.0.0.1:30120 --password your_rcon_password',\n },\n ];\n\n public async run(): Promise<void> {\n const { env, resourceName, rconOptions, watch } = await this._parseOptions();\n\n const config = createFivemRspackConfig({ environment: env, resourceName });\n const compiler = rspack(config);\n\n const reloadFivemResource = async () => {\n if (!rconOptions) return;\n this.logger.info(`Reloading FiveM resource \"${resourceName}\"...`);\n try {\n await refreshAndEnsureFivemResource(resourceName, rconOptions);\n this.logger.success(`FiveM resource reloaded successfully.\\n`);\n } catch (error) {\n this.logger.error(`Failed to reload FiveM resource: ${getErrMsg(error)}\\n`);\n }\n };\n\n const compilerCallback = (err: Error | null, stats: MultiStats | undefined) => {\n if (err) {\n this.logger.error(getErrMsg(err));\n if ('details' in err) this.logger.error(err.details as string);\n if ('stack' in err) this.logger.error(err.stack);\n return;\n }\n\n if (stats) this.logger.log(stats.toString(true), '\\n');\n\n void reloadFivemResource();\n };\n\n if (watch) {\n this.logger.info(`Building FiveM resource in watch mode...\\n`);\n compiler.watch({}, compilerCallback);\n } else {\n this.logger.info(`Building FiveM resource...\\n`);\n compiler.run(compilerCallback);\n }\n }\n\n private async _parseOptions(): Promise<FivemBuildCommandOptions> {\n const { flags } = await this.parse(FivemBuildCommand);\n\n const env = (flags.env ?? (flags.watch ? 'development' : 'production')) as Environment;\n const resourceName = flags.resourceName ?? path.basename(process.cwd());\n\n let rconOptions: RconOptions | undefined;\n\n if (flags.autoReload) {\n if (!flags.password) {\n throw new Error(\n 'RCON password is required for auto-reload. Please provide it using the --password flag.',\n );\n }\n\n const match = /^(?<host>[^:]+):(?<port>\\d+)$/u.exec(flags.server);\n const { host, port } = match?.groups ?? {};\n\n if (!host || !port) {\n throw new Error(\n `Invalid server address format: ${flags.server}. Expected format is \"ip:port\".`,\n );\n }\n\n rconOptions = {\n host,\n port: parseInt(port),\n password: flags.password,\n };\n }\n\n return { env, resourceName, rconOptions, watch: flags.watch };\n }\n}\n","import { FivemBuildCommand } from './fivem-build-command';\n\nexport const commands = {\n 'build:fivem': FivemBuildCommand,\n};\n"],"mappings":";;;;;;;;;AAWA,SAAS,aAAa,SAAiB,UAA0B;CAC7D,MAAM,SAAS,OAAO,MAAM,KAAK,SAAS,SAAS,QAAQ,OAAO;AAClE,QAAO,cAAc,YAAY,EAAE;AACnC,QAAO,MAAM,SAAS,EAAE;AACxB,QAAO,MAAM,UAAU,GAAG,SAAS,OAAO;AAC1C,QAAO,MAAM,KAAK,IAAI,SAAS,QAAQ,EAAE;AACzC,QAAO,MAAM,SAAS,KAAK,SAAS,QAAQ,QAAQ,OAAO;AAC3D,QAAO,MAAM,MAAM,KAAK,SAAS,SAAS,QAAQ,QAAQ,EAAE;AAC5D,QAAO;;AAGX,eAAsB,cAAc,SAAiB,SAAuC;CACxF,MAAM,SAAS,MAAM,aAAa,OAAO;AAEzC,QAAO,IAAI,SAAiB,SAAS,WAAW;EAC5C,IAAIA;EAEJ,MAAM,eAAe,QAAgC;AACjD,OAAI,CAAC,IAAK;AACV,gBAAa,UAAU;GACvB,MAAM,MAAM,UAAU,IAAI;AAC1B,0BAAO,IAAI,MAAM,6BAA6B,QAAQ,KAAK,GAAG,QAAQ,KAAK,IAAI,MAAM,CAAC;;EAG1F,MAAM,iBAAiB,QAAgB;AACnC,gBAAa,UAAU;GACvB,MAAM,MAAM,IAAI,SAAS,QAAQ,CAAC,MAAM,EAAE,CAAC,MAAM;AACjD,OAAI,IAAI,SAAS,mBAAmB,CAAE,QAAO,KAAK,YAAY,mBAAmB;AACjF,WAAQ,IAAI;;AAGhB,cAAY,iBACF,YAAY,iBAAiB,QAAQ,QAAQ,IAAI,EACvD,QAAQ,WAAW,IACtB;AAED,SAAO,KAAK,SAAS,YAAY;AACjC,SAAO,KAAK,WAAW,cAAc;EAErC,MAAM,MAAM,aAAa,SAAS,QAAQ,SAAS;AACnD,SAAO,KAAK,KAAK,GAAG,IAAI,QAAQ,QAAQ,MAAM,QAAQ,MAAM,YAAY;GAC1E,CAAC,cAAc;AACb,SAAO,OAAO;GAChB;;AAGN,eAAsB,8BAClB,cACA,SACa;CACb,MAAM,MAAM,MAAM,cAAc,mBAAmB,gBAAgB,QAAQ;AAC3E,KAAI,IAAI,SAAS,yBAAyB,CACtC,OAAM,IAAI,MAAM,aAAa,aAAa,aAAa;AAC3D,KAAI,CAAC,IAAI,SAAS,mBAAmB,CACjC,OAAM,IAAI,MAAM,6BAA6B,aAAa,GAAG;;;;;AC5CrE,IAAa,oBAAb,MAAa,0BAA0B,QAAQ;CAC3C,OAAgB,UAAU;CAE1B,OAAgB,QAAQ;EACpB,OAAO,MAAM,QAAQ;GACjB,MAAM;GACN,aAAa;GACb,SAAS;GACZ,CAAC;EACF,KAAK,MAAM,OAAO;GACd,MAAM;GACN,aAAa;GACb,SAAS,CAAC,eAAe,aAAa;GACtC,UAAU;GACb,CAAC;EACF,YAAY,MAAM,QAAQ;GACtB,MAAM;GACN,aAAa;GACb,SAAS;GACZ,CAAC;EACF,QAAQ,MAAM,OAAO;GACjB,MAAM;GACN,aAAa;GACb,SAAS;GACT,UAAU;GACb,CAAC;EACF,UAAU,MAAM,OAAO;GACnB,MAAM;GACN,aAAa;GACb,UAAU;GACb,CAAC;EACF,cAAc,MAAM,OAAO;GACvB,MAAM;GACN,aACI;GACJ,UAAU;GACb,CAAC;EACL;CAED,OAAgB,WAAW;EACvB;GACI,aAAa;GACb,SAAS;GACZ;EACD;GACI,aAAa;GACb,SAAS;GACZ;EACD;GACI,aAAa;GACb,SAAS;GACZ;EACD;GACI,aACI;GACJ,SACI;GACP;EACD;GACI,aACI;GACJ,SACI;GACP;EACD;GACI,aACI;GACJ,SACI;GACP;EACJ;CAED,MAAa,MAAqB;EAC9B,MAAM,EAAE,KAAK,cAAc,aAAa,UAAU,MAAM,KAAK,eAAe;EAG5E,MAAM,WAAW,OADF,wBAAwB;GAAE,aAAa;GAAK;GAAc,CAAC,CAC3C;EAE/B,MAAM,sBAAsB,YAAY;AACpC,OAAI,CAAC,YAAa;AAClB,QAAK,OAAO,KAAK,6BAA6B,aAAa,MAAM;AACjE,OAAI;AACA,UAAM,8BAA8B,cAAc,YAAY;AAC9D,SAAK,OAAO,QAAQ,0CAA0C;YACzD,OAAO;AACZ,SAAK,OAAO,MAAM,oCAAoC,UAAU,MAAM,CAAC,IAAI;;;EAInF,MAAM,oBAAoB,KAAmB,UAAkC;AAC3E,OAAI,KAAK;AACL,SAAK,OAAO,MAAM,UAAU,IAAI,CAAC;AACjC,QAAI,aAAa,IAAK,MAAK,OAAO,MAAM,IAAI,QAAkB;AAC9D,QAAI,WAAW,IAAK,MAAK,OAAO,MAAM,IAAI,MAAM;AAChD;;AAGJ,OAAI,MAAO,MAAK,OAAO,IAAI,MAAM,SAAS,KAAK,EAAE,KAAK;AAEtD,GAAK,qBAAqB;;AAG9B,MAAI,OAAO;AACP,QAAK,OAAO,KAAK,6CAA6C;AAC9D,YAAS,MAAM,EAAE,EAAE,iBAAiB;SACjC;AACH,QAAK,OAAO,KAAK,+BAA+B;AAChD,YAAS,IAAI,iBAAiB;;;CAItC,MAAc,gBAAmD;EAC7D,MAAM,EAAE,UAAU,MAAM,KAAK,MAAM,kBAAkB;EAErD,MAAM,MAAO,MAAM,QAAQ,MAAM,QAAQ,gBAAgB;EACzD,MAAM,eAAe,MAAM,gBAAgB,KAAK,SAAS,QAAQ,KAAK,CAAC;EAEvE,IAAIC;AAEJ,MAAI,MAAM,YAAY;AAClB,OAAI,CAAC,MAAM,SACP,OAAM,IAAI,MACN,0FACH;GAIL,MAAM,EAAE,MAAM,SADA,iCAAiC,KAAK,MAAM,OAAO,EACnC,UAAU,EAAE;AAE1C,OAAI,CAAC,QAAQ,CAAC,KACV,OAAM,IAAI,MACN,kCAAkC,MAAM,OAAO,iCAClD;AAGL,iBAAc;IACV;IACA,MAAM,SAAS,KAAK;IACpB,UAAU,MAAM;IACnB;;AAGL,SAAO;GAAE;GAAK;GAAc;GAAa,OAAO,MAAM;GAAO;;;;;;ACjKrE,MAAa,WAAW,EACpB,eAAe,mBAClB"}
@@ -13,8 +13,8 @@
13
13
  "command": "<%= config.bin %> <%= command.id %> --watch"
14
14
  },
15
15
  {
16
- "description": "Build the FiveM resource in production mode.",
17
- "command": "<%= config.bin %> <%= command.id %> --mode=production"
16
+ "description": "Build the FiveM resource in production environment.",
17
+ "command": "<%= config.bin %> <%= command.id %> --env=production"
18
18
  },
19
19
  {
20
20
  "description": "Build the FiveM resource and automatically reload it on the server after build.",
@@ -37,10 +37,10 @@
37
37
  "allowNo": false,
38
38
  "type": "boolean"
39
39
  },
40
- "mode": {
41
- "char": "m",
42
- "description": "Set the build mode (development or production).",
43
- "name": "mode",
40
+ "env": {
41
+ "char": "e",
42
+ "description": "Set the build environment (development or production).",
43
+ "name": "env",
44
44
  "required": false,
45
45
  "hasDynamicHelp": false,
46
46
  "multiple": false,
@@ -96,5 +96,5 @@
96
96
  "summary": "Build the FiveM resource using predefined config."
97
97
  }
98
98
  },
99
- "version": "0.0.13"
99
+ "version": "0.0.15"
100
100
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jpp-toolkit/plugin-build-fivem",
3
- "version": "0.0.13",
3
+ "version": "0.0.15",
4
4
  "description": "Plugin that add the fivem build command to the jpp cli.",
5
5
  "keywords": [
6
6
  "jpp",
@@ -36,9 +36,8 @@
36
36
  "dependencies": {
37
37
  "@oclif/core": "4.8.0",
38
38
  "@rspack/core": "1.6.7",
39
- "@jpp-toolkit/core": "0.0.15",
40
- "@jpp-toolkit/rcon": "0.0.4",
41
- "@jpp-toolkit/rspack-config": "0.0.3",
39
+ "@jpp-toolkit/core": "0.0.16",
40
+ "@jpp-toolkit/rspack-config": "0.0.5",
42
41
  "@jpp-toolkit/utils": "0.0.14"
43
42
  },
44
43
  "devDependencies": {
@@ -1,15 +1,23 @@
1
1
  import path from 'node:path';
2
2
 
3
3
  import { Command } from '@jpp-toolkit/core';
4
- import { FivemRcon } from '@jpp-toolkit/rcon';
5
4
  import { createFivemRspackConfig } from '@jpp-toolkit/rspack-config';
6
5
  import { getErrMsg } from '@jpp-toolkit/utils';
7
6
  import { Flags } from '@oclif/core';
8
- import type { Compiler, Stats } from '@rspack/core';
7
+ import type { MultiStats } from '@rspack/core';
9
8
  import { rspack } from '@rspack/core';
10
9
 
11
- type Mode = 'development' | 'production';
12
- type ServerAddress = { readonly host: string; readonly port: number };
10
+ import { refreshAndEnsureFivemResource } from './fivem-rcon';
11
+ import type { RconOptions } from './fivem-rcon';
12
+
13
+ type Environment = 'development' | 'production';
14
+
15
+ type FivemBuildCommandOptions = {
16
+ readonly env: Environment;
17
+ readonly resourceName: string;
18
+ readonly rconOptions?: RconOptions | undefined;
19
+ readonly watch: boolean;
20
+ };
13
21
 
14
22
  export class FivemBuildCommand extends Command {
15
23
  static override summary = 'Build the FiveM resource using predefined config.';
@@ -20,9 +28,9 @@ export class FivemBuildCommand extends Command {
20
28
  description: 'Watch files for changes and rebuild automatically.',
21
29
  default: false,
22
30
  }),
23
- mode: Flags.string({
24
- char: 'm',
25
- description: 'Set the build mode (development or production).',
31
+ env: Flags.string({
32
+ char: 'e',
33
+ description: 'Set the build environment (development or production).',
26
34
  options: ['development', 'production'],
27
35
  required: false,
28
36
  }),
@@ -60,8 +68,8 @@ export class FivemBuildCommand extends Command {
60
68
  command: '<%= config.bin %> <%= command.id %> --watch',
61
69
  },
62
70
  {
63
- description: 'Build the FiveM resource in production mode.',
64
- command: '<%= config.bin %> <%= command.id %> --mode=production',
71
+ description: 'Build the FiveM resource in production environment.',
72
+ command: '<%= config.bin %> <%= command.id %> --env=production',
65
73
  },
66
74
  {
67
75
  description:
@@ -84,80 +92,75 @@ export class FivemBuildCommand extends Command {
84
92
  ];
85
93
 
86
94
  public async run(): Promise<void> {
87
- const { flags } = await this.parse(FivemBuildCommand);
88
-
89
- const { watch, autoReload, password } = flags;
90
- const mode = (flags.mode ?? (watch ? 'development' : 'production')) as Mode;
91
- const { host, port } = this._parseServerAddress(flags.server);
92
- const resourceName = flags.resourceName ?? path.basename(process.cwd());
95
+ const { env, resourceName, rconOptions, watch } = await this._parseOptions();
93
96
 
94
- const config = createFivemRspackConfig({ mode });
97
+ const config = createFivemRspackConfig({ environment: env, resourceName });
95
98
  const compiler = rspack(config);
96
99
 
97
- if (autoReload) {
98
- if (!password) {
99
- this.fatalError(
100
- 'RCON password is required for auto-reload. Please provide it using the --password flag.',
101
- );
100
+ const reloadFivemResource = async () => {
101
+ if (!rconOptions) return;
102
+ this.logger.info(`Reloading FiveM resource "${resourceName}"...`);
103
+ try {
104
+ await refreshAndEnsureFivemResource(resourceName, rconOptions);
105
+ this.logger.success(`FiveM resource reloaded successfully.\n`);
106
+ } catch (error) {
107
+ this.logger.error(`Failed to reload FiveM resource: ${getErrMsg(error)}\n`);
102
108
  }
109
+ };
103
110
 
104
- this._enableAutoReload(compiler, resourceName, host, port, password);
105
- }
111
+ const compilerCallback = (err: Error | null, stats: MultiStats | undefined) => {
112
+ if (err) {
113
+ this.logger.error(getErrMsg(err));
114
+ if ('details' in err) this.logger.error(err.details as string);
115
+ if ('stack' in err) this.logger.error(err.stack);
116
+ return;
117
+ }
118
+
119
+ if (stats) this.logger.log(stats.toString(true), '\n');
106
120
 
107
- if (flags.watch) {
121
+ void reloadFivemResource();
122
+ };
123
+
124
+ if (watch) {
108
125
  this.logger.info(`Building FiveM resource in watch mode...\n`);
109
- compiler.watch({}, (err, stats) => this._compilerCallback(err, stats));
126
+ compiler.watch({}, compilerCallback);
110
127
  } else {
111
128
  this.logger.info(`Building FiveM resource...\n`);
112
- compiler.run((err, stats) => this._compilerCallback(err, stats));
129
+ compiler.run(compilerCallback);
113
130
  }
114
131
  }
115
132
 
116
- private _parseServerAddress(address: string): ServerAddress {
117
- const match = /^(?<host>[^:]+):(?<port>\d+)$/u.exec(address);
118
- const { host, port } = match?.groups ?? {};
133
+ private async _parseOptions(): Promise<FivemBuildCommandOptions> {
134
+ const { flags } = await this.parse(FivemBuildCommand);
119
135
 
120
- if (!host || !port) {
121
- throw new Error(
122
- `Invalid server address format: ${address}. Expected format is "ip:port".`,
123
- );
124
- }
136
+ const env = (flags.env ?? (flags.watch ? 'development' : 'production')) as Environment;
137
+ const resourceName = flags.resourceName ?? path.basename(process.cwd());
125
138
 
126
- return { host, port: parseInt(port) };
127
- }
139
+ let rconOptions: RconOptions | undefined;
128
140
 
129
- private _compilerCallback(err: Error | null, stats: Stats | undefined): void {
130
- if (err) {
131
- this.logger.error(getErrMsg(err));
132
- if ('details' in err) this.logger.error(err.details as string);
133
- if ('stack' in err) this.logger.error(err.stack);
134
- return;
135
- }
141
+ if (flags.autoReload) {
142
+ if (!flags.password) {
143
+ throw new Error(
144
+ 'RCON password is required for auto-reload. Please provide it using the --password flag.',
145
+ );
146
+ }
136
147
 
137
- if (stats) this.logger.log(stats.toString(), '\n');
138
- }
148
+ const match = /^(?<host>[^:]+):(?<port>\d+)$/u.exec(flags.server);
149
+ const { host, port } = match?.groups ?? {};
139
150
 
140
- private _enableAutoReload(
141
- compiler: Compiler,
142
- resourceName: string,
143
- host: string,
144
- port: number,
145
- password: string,
146
- ): void {
147
- compiler.hooks.done.tapPromise('FivemAutoReloadPlugin', async (stats) => {
148
- if (stats.hasErrors()) {
149
- this.logger.warning('Build failed. Skipping FiveM resource reload.\n');
150
- return;
151
+ if (!host || !port) {
152
+ throw new Error(
153
+ `Invalid server address format: ${flags.server}. Expected format is "ip:port".`,
154
+ );
151
155
  }
152
156
 
153
- this.logger.info(`Reloading FiveM resource "${resourceName}"...`);
154
- try {
155
- const rcon = new FivemRcon({ host, port, password });
156
- await rcon.refreshAndEnsureResource(resourceName);
157
- this.logger.success(`FiveM resource reloaded successfully.\n`);
158
- } catch (error) {
159
- this.logger.error(`Failed to reload FiveM resource: ${getErrMsg(error)}\n`);
160
- }
161
- });
157
+ rconOptions = {
158
+ host,
159
+ port: parseInt(port),
160
+ password: flags.password,
161
+ };
162
+ }
163
+
164
+ return { env, resourceName, rconOptions, watch: flags.watch };
162
165
  }
163
166
  }
@@ -0,0 +1,67 @@
1
+ import dgram from 'node:dgram';
2
+
3
+ import { getErrMsg } from '@jpp-toolkit/utils';
4
+
5
+ export type RconOptions = {
6
+ readonly host: string;
7
+ readonly port: number;
8
+ readonly password: string;
9
+ readonly timeout?: number | undefined;
10
+ };
11
+
12
+ function buildCommand(command: string, password: string): Buffer {
13
+ const buffer = Buffer.alloc(11 + password.length + command.length);
14
+ buffer.writeUInt32LE(0xffffffff, 0);
15
+ buffer.write('rcon ', 4);
16
+ buffer.write(password, 9, password.length);
17
+ buffer.write(' ', 9 + password.length, 1);
18
+ buffer.write(command, 10 + password.length, command.length);
19
+ buffer.write('\n', 10 + password.length + command.length, 1);
20
+ return buffer;
21
+ }
22
+
23
+ export async function sendFivemRcon(command: string, options: RconOptions): Promise<string> {
24
+ const socket = dgram.createSocket('udp4');
25
+
26
+ return new Promise<string>((resolve, reject) => {
27
+ let timeoutId: NodeJS.Timeout;
28
+
29
+ const handleError = (err?: Error | string | null) => {
30
+ if (!err) return;
31
+ clearTimeout(timeoutId);
32
+ const msg = getErrMsg(err);
33
+ reject(new Error(`Failed to send command to ${options.host}:${options.port}: ${msg}`));
34
+ };
35
+
36
+ const handleMessage = (msg: Buffer) => {
37
+ clearTimeout(timeoutId);
38
+ const res = msg.toString('ascii').slice(4).trim();
39
+ if (res.includes('Invalid password')) return void handleError('Invalid password');
40
+ resolve(res);
41
+ };
42
+
43
+ timeoutId = setTimeout(
44
+ () => handleError(`Timeout after ${options.timeout}ms`),
45
+ options.timeout ?? 5000,
46
+ );
47
+
48
+ socket.once('error', handleError);
49
+ socket.once('message', handleMessage);
50
+
51
+ const cmd = buildCommand(command, options.password);
52
+ socket.send(cmd, 0, cmd.length, options.port, options.host, handleError);
53
+ }).finally(() => {
54
+ socket.close();
55
+ });
56
+ }
57
+
58
+ export async function refreshAndEnsureFivemResource(
59
+ resourceName: string,
60
+ options: RconOptions,
61
+ ): Promise<void> {
62
+ const res = await sendFivemRcon(`refresh; ensure ${resourceName}`, options);
63
+ if (res.includes("Couldn't find resource"))
64
+ throw new Error(`Resource "${resourceName}" not found`);
65
+ if (!res.includes('Started resource'))
66
+ throw new Error(`Failed to start resource "${resourceName}"`);
67
+ }