@bf6mods/cli 1.0.1 → 1.1.0

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/README.md CHANGED
@@ -7,15 +7,17 @@ Create Battlefield 6 mods quickly with a vite like development experience.
7
7
  To create a new project, simply run the following command and answer the questions
8
8
 
9
9
  ```
10
- npx bf6mods init
10
+ npx @bf6mods/cli init
11
11
  ```
12
12
 
13
+ ![Initializing a repo](https://raw.githubusercontent.com/bf6mods/bf6mods/refs/heads/main/media/terminal.gif "Initializing a repo")
14
+
13
15
  ## Import from JSON
14
16
 
15
17
  Already have a project? Just export your currently existing project in [portal.battlefield.com](https://portal.battlefield.com), and run...
16
18
 
17
19
  ```
18
- npx bf6mods import <input> <output>
20
+ npx @bf6mods/cli import <input> <output>
19
21
  ```
20
22
 
21
23
  ## Deploying Project to Portal
@@ -27,8 +29,9 @@ Just run `npm run build` in your project dir, open [portal.battlefield.com](http
27
29
  - Use multiple different files, instead of just one large TypeScript file!
28
30
  - Use a programatic interface for defining your gamemode.
29
31
  - Extensive documentation
30
- - Extended standard library
32
+ - Extended standard library (still in progress)
31
33
  - Hot reload
34
+ - Automatic string injection
32
35
 
33
36
  ## Structure
34
37
 
@@ -68,3 +71,7 @@ export default defineBf6Config({
68
71
  ## @bf6mods/sdk
69
72
 
70
73
  This is a seperate library that exports the `PortalSdk`'s `mod` and `modlib`. Additionally it exports some stdlib helper functions and classes to help accelerate development.
74
+
75
+ ## Join the discord with fellow modders!
76
+
77
+ You can join the discord by clicking this [link](https://discord.gg/2gJ9fheYYK)!
package/dist/cli/index.js CHANGED
@@ -6,7 +6,7 @@ import { Command } from "commander";
6
6
  // package.json
7
7
  var package_default = {
8
8
  name: "@bf6mods/cli",
9
- version: "1.0.1",
9
+ version: "1.0.2",
10
10
  description: "CLI and library for bundling BF6 mods",
11
11
  license: "MIT",
12
12
  type: "module",
@@ -28,8 +28,12 @@ var package_default = {
28
28
  files: [
29
29
  "dist/"
30
30
  ],
31
+ repository: {
32
+ type: "git",
33
+ url: "git+https://github.com/bf6mods/bf6mods.git"
34
+ },
31
35
  dependencies: {
32
- "@bf6mods/sdk": "1.0.1",
36
+ "@bf6mods/sdk": "1.0.3",
33
37
  "@clack/prompts": "^0.11.0",
34
38
  chokidar: "^4.0.3",
35
39
  colors: "^1.4.0",
@@ -43,15 +47,17 @@ var package_default = {
43
47
  access: "public"
44
48
  },
45
49
  devDependencies: {
50
+ "@oxc-project/types": "^0.95.0",
46
51
  "@types/stringify-object": "^4.0.5",
47
52
  "cross-env": "^10.1.0",
48
53
  tsx: "^4.20.6"
49
54
  }
50
55
  };
51
56
 
52
- // src/cli/build.ts
57
+ // src/cli/build/index.ts
53
58
  import fs from "fs";
54
59
  import path from "path";
60
+ import { fileURLToPath } from "url";
55
61
  import {
56
62
  AttachmentType
57
63
  } from "@bf6mods/sdk";
@@ -72,7 +78,156 @@ var MapId = /* @__PURE__ */ ((MapId2) => {
72
78
  return MapId2;
73
79
  })(MapId || {});
74
80
 
75
- // src/cli/build.ts
81
+ // src/cli/build/generated-strings.ts
82
+ function extractBf6Strings(bf6Strings, generateFromLiterals) {
83
+ return {
84
+ name: "extract-bf6-strings",
85
+ generateBundle(_options, bundle) {
86
+ if (!generateFromLiterals) return;
87
+ let id = 0;
88
+ const existingStrings = new Set(Object.values(bf6Strings));
89
+ for (const [_file, output] of Object.entries(bundle)) {
90
+ let walk2 = function(node) {
91
+ if (node.type === "Literal") {
92
+ if (typeof node.value !== "string" || node.value === "" || node.value === "----uniquename----")
93
+ return;
94
+ if (!(node.value in bf6Strings) && !existingStrings.has(node.value)) {
95
+ existingStrings.add(node.value);
96
+ bf6Strings[`__auto__${id++}`] = node.value;
97
+ }
98
+ }
99
+ for (const value of Object.values(node)) {
100
+ if (!value) continue;
101
+ if (Array.isArray(value)) {
102
+ for (const v of value) {
103
+ if (v && typeof v === "object" && "type" in v) walk2(v);
104
+ }
105
+ } else if (typeof value === "object" && "type" in value) {
106
+ walk2(value);
107
+ }
108
+ }
109
+ };
110
+ var walk = walk2;
111
+ if (output.type !== "chunk") continue;
112
+ const code = output.code;
113
+ const program2 = this.parse(code, {
114
+ lang: "ts",
115
+ astType: "js",
116
+ range: true
117
+ });
118
+ for (const item of program2.body) {
119
+ walk2(item);
120
+ }
121
+ }
122
+ },
123
+ moduleParsed(moduleInfo) {
124
+ if (moduleInfo.code && moduleInfo.exports.includes("bf6Strings")) {
125
+ const code = moduleInfo.code;
126
+ const getSnippet = (start, end, linesBefore = 2, linesAfter = 2) => {
127
+ const lines = code.split(/\r?\n/);
128
+ const startLine = Math.max(
129
+ 0,
130
+ code.slice(0, start).split(/\r?\n/).length - 1 - linesBefore
131
+ );
132
+ const endLine = Math.min(
133
+ lines.length,
134
+ code.slice(0, end).split(/\r?\n/).length - 1 + linesAfter
135
+ );
136
+ return lines.slice(startLine, endLine).map((line, i) => {
137
+ const actualLine = startLine + i + 1;
138
+ return `${actualLine.toString().padStart(3)} | ${line}`;
139
+ }).join("\n");
140
+ };
141
+ const program2 = this.parse(code, {
142
+ lang: "ts",
143
+ astType: "js",
144
+ range: true
145
+ });
146
+ const extractObjectLiteral = (declaration) => {
147
+ if (declaration.init?.type !== "ObjectExpression") {
148
+ const snippet = getSnippet(...declaration.range ?? [0, 0]);
149
+ this.error(
150
+ `\u274C Invalid bf6Strings export: expected an object literal.
151
+
152
+ ${snippet}`
153
+ );
154
+ }
155
+ for (const property of declaration.init.properties) {
156
+ if (property.type !== "Property") {
157
+ const snippet = getSnippet(...property.range ?? [0, 0]);
158
+ this.error(
159
+ `\u274C Invalid bf6Strings property: expected a key/value pair.
160
+ Found type: ${property.type}
161
+
162
+ ${snippet}`
163
+ );
164
+ }
165
+ let key = void 0;
166
+ if (property.key.type === "Literal") {
167
+ key = property.key.value;
168
+ } else if (property.key.type === "Identifier") {
169
+ key = property.key.name;
170
+ } else {
171
+ const snippet = getSnippet(...property.range ?? [0, 0]);
172
+ this.error(
173
+ `\u274C Invalid bf6Strings key: expected a string literal or identifier.
174
+ Found type: ${property.key.type}
175
+
176
+ ${snippet}`
177
+ );
178
+ continue;
179
+ }
180
+ if (property.value.type !== "Literal") {
181
+ const snippet = getSnippet(...property.range ?? [0, 0]);
182
+ this.error(
183
+ `\u274C Invalid bf6Strings value: expected a string literal.
184
+ Found type: ${property.value.type}
185
+
186
+ ${snippet}`
187
+ );
188
+ }
189
+ const value = property.value.value;
190
+ if (typeof key !== "string" && typeof key !== "number" && typeof key !== "bigint" && typeof key !== "boolean") {
191
+ const snippet = getSnippet(...property.range ?? [0, 0]);
192
+ this.error(
193
+ `\u274C bf6Strings key must be a string, found: ${typeof key}
194
+
195
+ ${snippet}`
196
+ );
197
+ }
198
+ if (typeof value !== "string" && typeof value !== "number" && typeof value !== "bigint" && typeof value !== "boolean") {
199
+ const snippet = getSnippet(...property.range ?? [0, 0]);
200
+ this.error(
201
+ `\u274C bf6Strings value must be a string, found: ${typeof value}
202
+
203
+ ${snippet}`
204
+ );
205
+ }
206
+ bf6Strings[`${key}`] = `${value}`;
207
+ }
208
+ };
209
+ for (const item of program2.body) {
210
+ if (item.type === "VariableDeclaration") {
211
+ for (const declaration of item.declarations) {
212
+ if (declaration.type === "VariableDeclarator" && declaration.id.type === "Identifier" && declaration.id.name === "bf6Strings") {
213
+ extractObjectLiteral(declaration);
214
+ }
215
+ }
216
+ }
217
+ if (item.type === "ExportNamedDeclaration" && item.declaration?.type === "VariableDeclaration") {
218
+ for (const declaration of item.declaration.declarations) {
219
+ if (declaration.type === "VariableDeclarator" && declaration.id.type === "Identifier" && declaration.id.name === "bf6Strings") {
220
+ extractObjectLiteral(declaration);
221
+ }
222
+ }
223
+ }
224
+ }
225
+ }
226
+ }
227
+ };
228
+ }
229
+
230
+ // src/cli/build/index.ts
76
231
  async function getBf6Config(rootDir) {
77
232
  globalThis.defineBf6Config = (c) => c;
78
233
  globalThis.MapId = MapId;
@@ -90,39 +245,43 @@ async function build() {
90
245
  fs.rmSync(outDir, { recursive: true, force: true });
91
246
  fs.mkdirSync(outDir, { recursive: true });
92
247
  const minifyJson = typeof config.minify === "boolean" ? config.minify : config.minify?.json ?? false;
248
+ const generatedStrings = {};
93
249
  let tsAttachment;
94
250
  if (config.entrypoint) {
95
251
  const entryAbs = path.resolve(workingDir, config.entrypoint);
96
252
  const compiled = await buildEntrypoint(
97
253
  entryAbs,
98
- outDir,
99
- !!config.outputArtifacts
254
+ generatedStrings,
255
+ config.generateStrings ?? true
100
256
  );
101
257
  tsAttachment = createTsAttachment(entryAbs, compiled);
102
258
  }
103
259
  const { attachments, mapRotation } = await collectAttachments(
104
260
  config,
105
261
  workingDir,
106
- tsAttachment
262
+ tsAttachment,
263
+ generatedStrings
107
264
  );
108
265
  await writeModJson(config, outDir, attachments, mapRotation, minifyJson);
109
266
  console.log(`\u2714 Built mod: ${config.name}`);
110
267
  }
111
- async function buildEntrypoint(entry, outDir, emit) {
268
+ async function buildEntrypoint(entry, bf6Strings, generateStringsFromLiterals) {
112
269
  const bundle = await rolldown({
113
- input: entry
270
+ input: entry,
271
+ plugins: [extractBf6Strings(bf6Strings, generateStringsFromLiterals)],
272
+ logLevel: "debug"
114
273
  });
115
274
  const result = await bundle.generate({
116
275
  format: "esm",
117
276
  inlineDynamicImports: true
118
277
  });
119
278
  const code = result.output[0].code;
120
- if (emit) {
121
- await fs.promises.writeFile(path.resolve(outDir, "index.js"), code);
122
- }
123
279
  return code;
124
280
  }
125
- async function collectAttachments(config, workingDir, tsAttachment) {
281
+ var __filename = fileURLToPath(import.meta.url);
282
+ var __dirname = path.dirname(__filename);
283
+ var spatialsDir = path.resolve(__dirname, "../resources/maps/spatial");
284
+ async function collectAttachments(config, workingDir, tsAttachment, generatedStrings) {
126
285
  const attachments = [];
127
286
  const mapRotation = [];
128
287
  if (tsAttachment) attachments.push(tsAttachment);
@@ -130,11 +289,20 @@ async function collectAttachments(config, workingDir, tsAttachment) {
130
289
  const strPath = path.resolve(workingDir, config.strings);
131
290
  if (!fs.existsSync(strPath)) throw new Error("Cannot find strings file");
132
291
  const raw = await fs.promises.readFile(strPath, "utf8");
133
- attachments.push(createStringsAttachment(strPath, raw));
292
+ const attachment = createStringsAttachment(strPath, raw, generatedStrings);
293
+ attachments.push(attachment);
134
294
  }
135
295
  if (config.scenes) {
136
296
  let mapIdx = 0;
137
- for (const [mapId, scene] of config.scenes) {
297
+ for (const map of config.scenes) {
298
+ let mapId;
299
+ let scene;
300
+ if (Array.isArray(map)) {
301
+ [mapId, scene] = map;
302
+ } else {
303
+ mapId = map;
304
+ scene = path.resolve(spatialsDir, `${mapId}.spatial.json`);
305
+ }
138
306
  const scenePath = path.resolve(workingDir, scene);
139
307
  if (!fs.existsSync(scenePath))
140
308
  throw new Error(`Cannot find spatial data file: ${scene}`);
@@ -160,6 +328,19 @@ async function writeModJson(config, outDir, attachments, mapRotation, minify) {
160
328
  };
161
329
  const jsonOutput = minify ? JSON.stringify(finalJson) : JSON.stringify(finalJson, null, 2);
162
330
  await fs.promises.writeFile(path.resolve(outDir, "mod.json"), jsonOutput);
331
+ if (config.outputArtifacts && finalJson?.attachments) {
332
+ for (const attachment of finalJson.attachments) {
333
+ const attachmentsDir = path.resolve(outDir, "attachments");
334
+ if (!fs.existsSync(attachmentsDir))
335
+ fs.mkdirSync(attachmentsDir, {
336
+ recursive: true
337
+ });
338
+ await fs.promises.writeFile(
339
+ path.resolve(outDir, "attachments", attachment.filename),
340
+ atob(attachment.attachmentData.original)
341
+ );
342
+ }
343
+ }
163
344
  }
164
345
  function createTsAttachment(filePath, compiled) {
165
346
  return {
@@ -173,7 +354,18 @@ function createTsAttachment(filePath, compiled) {
173
354
  errors: []
174
355
  };
175
356
  }
176
- function createStringsAttachment(filePath, raw) {
357
+ function createStringsAttachment(filePath, raw, generatedStrings) {
358
+ let result = raw;
359
+ if (generatedStrings) {
360
+ result = JSON.stringify(
361
+ {
362
+ ...generatedStrings,
363
+ ...JSON.parse(raw)
364
+ },
365
+ null,
366
+ 4
367
+ );
368
+ }
177
369
  return {
178
370
  id: crypto.randomUUID(),
179
371
  version: "1.0",
@@ -181,7 +373,7 @@ function createStringsAttachment(filePath, raw) {
181
373
  isProcessable: true,
182
374
  processingStatus: 2,
183
375
  attachmentType: AttachmentType.Strings,
184
- attachmentData: { original: toBase64(raw), compiled: "" },
376
+ attachmentData: { original: toBase64(result), compiled: "" },
185
377
  errors: []
186
378
  };
187
379
  }
@@ -293,11 +485,11 @@ import stringifyObject from "stringify-object";
293
485
  import child_process from "child_process";
294
486
  import fs3 from "fs";
295
487
  import path3 from "path";
296
- import { fileURLToPath } from "url";
488
+ import { fileURLToPath as fileURLToPath2 } from "url";
297
489
  import * as prompts from "@clack/prompts";
298
- var __filename = fileURLToPath(import.meta.url);
299
- var __dirname = path3.dirname(__filename);
300
- var templatesDir = path3.resolve(__dirname, "../resources/templates");
490
+ var __filename2 = fileURLToPath2(import.meta.url);
491
+ var __dirname2 = path3.dirname(__filename2);
492
+ var templatesDir = path3.resolve(__dirname2, "../resources/templates");
301
493
  function renameFilesRecursively(dir, modName) {
302
494
  const entries = fs3.readdirSync(dir, { withFileTypes: true });
303
495
  for (const entry of entries) {
@@ -540,7 +732,7 @@ function stringifyWithRaw(value, options = {}) {
540
732
  // src/cli/prepare.ts
541
733
  import fs5 from "fs";
542
734
  import path5 from "path";
543
- import { fileURLToPath as fileURLToPath2 } from "url";
735
+ import { fileURLToPath as fileURLToPath3 } from "url";
544
736
  import colors3 from "colors";
545
737
  import { genExport, genInlineTypeImport } from "knitwork";
546
738
 
@@ -580,11 +772,11 @@ var printToConsole = (message) => {
580
772
  // src/cli/prepare.ts
581
773
  async function prepare() {
582
774
  try {
583
- const __filename2 = fileURLToPath2(import.meta.url);
584
- const __dirname2 = path5.dirname(__filename2);
775
+ const __filename3 = fileURLToPath3(import.meta.url);
776
+ const __dirname3 = path5.dirname(__filename3);
585
777
  const _workingDir = path5.resolve(".");
586
778
  const buildDir = path5.resolve(".bf6");
587
- const resources = path5.resolve(__dirname2, "../resources/prepare");
779
+ const resources = path5.resolve(__dirname3, "../resources/prepare");
588
780
  fs5.cpSync(
589
781
  path5.resolve(resources, "tsconfig.json"),
590
782
  path5.resolve(buildDir, "tsconfig.json")