msfs-layout-generator 0.3.3 → 0.3.5

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/cli.js CHANGED
@@ -84,6 +84,8 @@ program
84
84
  .option('-d, --debug', 'Enable debug logging for troubleshooting')
85
85
  .option('--no-manifest-check', 'Skip manifest.json existence check')
86
86
  .option('-w, --watch', 'Watch directory for changes and regenerate automatically')
87
+ .option('--watch-debounce <ms>', 'Debounce delay in watch mode (default: 750ms)', '750')
88
+ .option('--watch-interval <ms>', 'Polling interval in watch mode when polling is used (default: 100)', '100')
87
89
  .action(async (directories, options) => {
88
90
  await handleAction(directories, options);
89
91
  })
@@ -244,8 +246,11 @@ async function handleWatchMode(dir, options) {
244
246
  }
245
247
  process.exit(1);
246
248
  }
249
+ const debounceMs = Number.parseInt(watchDebounce ?? '750', 10);
250
+ const intervalMs = Number.parseInt(watchInterval ?? '100', 10);
247
251
  let debounceTimer;
248
252
  let isProcessing = false;
253
+ let hasPendingChanges = false;
249
254
  let changeCount = 0;
250
255
  const watcher = chokidar_1.default.watch(fullPath, {
251
256
  ignored: [
@@ -254,39 +259,46 @@ async function handleWatchMode(dir, options) {
254
259
  ],
255
260
  ignoreInitial: true,
256
261
  persistent: true,
257
- interval: parseInt(watchInterval),
262
+ interval: Number.isFinite(intervalMs) ? intervalMs : 100,
258
263
  depth: 99
259
264
  });
260
- const processChanges = async () => {
265
+ const processQueuedChanges = async () => {
261
266
  if (isProcessing) {
262
- if (debug) {
263
- logger.dim('Skipping - already processing');
264
- }
267
+ // Keep a rerun request so events arriving during a write/copy are not lost.
268
+ hasPendingChanges = true;
265
269
  return;
266
270
  }
267
- clearTimeout(debounceTimer);
268
- debounceTimer = setTimeout(async () => {
269
- isProcessing = true;
270
- changeCount++;
271
- try {
271
+ isProcessing = true;
272
+ try {
273
+ do {
274
+ hasPendingChanges = false;
275
+ changeCount++;
272
276
  await (0, processLayout_1.doProcessLayoutFileCli)(fullPath, {
273
277
  force: true,
274
278
  quiet: true
275
279
  });
276
- }
277
- catch (error) {
278
- const timestamp = new Date().toLocaleTimeString();
279
- if (!quiet) {
280
- logger.error(`[${timestamp}] Failed to regenerate: ${error.message}`);
281
- if (debug && error.stack) {
282
- logger.dim(error.stack);
283
- }
280
+ } while (hasPendingChanges);
281
+ }
282
+ catch (error) {
283
+ const timestamp = new Date().toLocaleTimeString();
284
+ if (!quiet) {
285
+ logger.error(`[${timestamp}] Failed to regenerate: ${error.message}`);
286
+ if (debug && error.stack) {
287
+ logger.dim(error.stack);
284
288
  }
285
289
  }
286
- finally {
287
- isProcessing = false;
288
- }
289
- }, parseInt(watchDebounce));
290
+ }
291
+ finally {
292
+ isProcessing = false;
293
+ }
294
+ };
295
+ const scheduleProcessing = () => {
296
+ if (debounceTimer) {
297
+ clearTimeout(debounceTimer);
298
+ }
299
+ debounceTimer = setTimeout(() => {
300
+ void processQueuedChanges();
301
+ }, Number.isFinite(debounceMs) ? debounceMs : 750);
290
302
  };
291
303
  const timestamp = new Date().toLocaleTimeString();
292
304
  watcher
@@ -294,31 +306,31 @@ async function handleWatchMode(dir, options) {
294
306
  if (!quiet) {
295
307
  logger.dim(`[${timestamp}] File added: ${path.relative(fullPath, filePath)}`);
296
308
  }
297
- processChanges();
309
+ scheduleProcessing();
298
310
  })
299
311
  .on('change', (filePath) => {
300
312
  if (!quiet) {
301
313
  logger.dim(`[${timestamp}] File changed: ${path.relative(fullPath, filePath)}`);
302
314
  }
303
- processChanges();
315
+ scheduleProcessing();
304
316
  })
305
317
  .on('unlink', (filePath) => {
306
318
  if (!quiet) {
307
319
  logger.dim(`[${timestamp}] File removed: ${path.relative(fullPath, filePath)}`);
308
320
  }
309
- processChanges();
321
+ scheduleProcessing();
310
322
  })
311
323
  .on('addDir', (dirPath) => {
312
324
  if (!quiet) {
313
325
  logger.dim(`[${timestamp}] Directory added: ${path.relative(fullPath, dirPath)}`);
314
326
  }
315
- processChanges();
327
+ scheduleProcessing();
316
328
  })
317
329
  .on('unlinkDir', (dirPath) => {
318
330
  if (!quiet) {
319
331
  logger.dim(`[${timestamp}] Directory removed: ${path.relative(fullPath, dirPath)}`);
320
332
  }
321
- processChanges();
333
+ scheduleProcessing();
322
334
  })
323
335
  .on('error', (error) => {
324
336
  if (error instanceof Error) {
package/dist/index.d.ts CHANGED
@@ -2,8 +2,8 @@ import { processLayout } from "./utils/processLayout";
2
2
  /**
3
3
  * Process an MSFS package directory to generate/update layout.json (simple API).
4
4
  *
5
- * This is a convenience wrapper around {@link processLayout} that uses default
6
- * options and throws on errors. For more control, use {@link processLayout} directly.
5
+ * This is a convenience wrapper around {@link processLayout} that throws on errors.
6
+ * You can pass the same processing options except `returnResult`.
7
7
  *
8
8
  * @param packageDir - Path to the MSFS package directory (must contain manifest.json)
9
9
  * @returns Promise that resolves when layout.json has been generated
@@ -16,6 +16,10 @@ import { processLayout } from "./utils/processLayout";
16
16
  * await generateLayout("F:\\fs20\\Community\\my-package");
17
17
  *
18
18
  * @example
19
+ * // Overwrite existing layout.json
20
+ * await generateLayout("./my-package", { force: true });
21
+ *
22
+ * @example
19
23
  * // With error handling
20
24
  * try {
21
25
  * await generateLayout("./my-package");
@@ -24,6 +28,6 @@ import { processLayout } from "./utils/processLayout";
24
28
  * console.error("Failed:", error.message);
25
29
  * }
26
30
  */
27
- export declare const generateLayout: (layoutPath: string) => Promise<void>;
31
+ export declare const generateLayout: (packageDir: string, options?: Omit<import("./types").ProcessOptions, "returnResult">) => Promise<void>;
28
32
  export { processLayout };
29
33
  export type { Content, Layout, Manifest, ProcessOptions, ProcessResult } from "./types";
package/dist/index.js CHANGED
@@ -6,8 +6,8 @@ Object.defineProperty(exports, "processLayout", { enumerable: true, get: functio
6
6
  /**
7
7
  * Process an MSFS package directory to generate/update layout.json (simple API).
8
8
  *
9
- * This is a convenience wrapper around {@link processLayout} that uses default
10
- * options and throws on errors. For more control, use {@link processLayout} directly.
9
+ * This is a convenience wrapper around {@link processLayout} that throws on errors.
10
+ * You can pass the same processing options except `returnResult`.
11
11
  *
12
12
  * @param packageDir - Path to the MSFS package directory (must contain manifest.json)
13
13
  * @returns Promise that resolves when layout.json has been generated
@@ -20,6 +20,10 @@ Object.defineProperty(exports, "processLayout", { enumerable: true, get: functio
20
20
  * await generateLayout("F:\\fs20\\Community\\my-package");
21
21
  *
22
22
  * @example
23
+ * // Overwrite existing layout.json
24
+ * await generateLayout("./my-package", { force: true });
25
+ *
26
+ * @example
23
27
  * // With error handling
24
28
  * try {
25
29
  * await generateLayout("./my-package");
@@ -41,22 +41,31 @@ const getAllFiles = async (dirPath) => {
41
41
  const files = [];
42
42
  async function readDirectory(currentPath) {
43
43
  try {
44
- for (const item of await (0, promises_1.readdir)(currentPath)) {
45
- const itemPath = path.join(currentPath, item);
46
- const receivedFile = await (0, promises_1.stat)(itemPath);
47
- if (receivedFile.isDirectory()) {
44
+ const directs = await (0, promises_1.readdir)(currentPath, { withFileTypes: true });
45
+ for (const dirent of directs) {
46
+ const itemPath = path.join(currentPath, dirent.name);
47
+ if (dirent.isDirectory()) {
48
48
  await readDirectory(itemPath);
49
49
  }
50
- else {
50
+ else if (dirent.isFile()) {
51
51
  files.push(itemPath);
52
52
  }
53
+ else if (dirent.isSymbolicLink()) {
54
+ const stats = await (0, promises_1.stat)(itemPath);
55
+ if (stats.isDirectory()) {
56
+ await readDirectory(itemPath);
57
+ }
58
+ else {
59
+ files.push(itemPath);
60
+ }
61
+ }
53
62
  }
54
63
  }
55
64
  catch (e) {
56
65
  if (e instanceof Error) {
57
- throw new _errors_1.ReadingDirError(`Failed to read directory ${path}: ${e.message}.`);
66
+ throw new _errors_1.ReadingDirError(`Failed to read directory ${currentPath}: ${e.message}.`);
58
67
  }
59
- throw new _errors_1.ReadingDirError(`Failed to read directory. Path: ${path}`);
68
+ throw new _errors_1.ReadingDirError(`Failed to read directory. Path: ${currentPath}`);
60
69
  }
61
70
  }
62
71
  await readDirectory(dirPath);
@@ -42,5 +42,5 @@ import { ProcessOptions, ProcessResult } from "../types";
42
42
  * });
43
43
  */
44
44
  export declare const processLayout: (packageDir: string, options?: ProcessOptions) => Promise<void | ProcessResult>;
45
- export declare const doProcessLayoutFile: (layoutPath: string) => Promise<void>;
45
+ export declare const doProcessLayoutFile: (packageDir: string, options?: Omit<ProcessOptions, "returnResult">) => Promise<void>;
46
46
  export declare const doProcessLayoutFileCli: (packageDir: string, options?: Omit<ProcessOptions, "returnResult">) => Promise<ProcessResult>;
@@ -202,15 +202,16 @@ const processLayout = async (packageDir, options = {}) => {
202
202
  throw new Error(error);
203
203
  }
204
204
  for (const file of allFiles) {
205
- // Check for long file paths (Windows limitation)
206
- if (file.length > 259) {
205
+ const relativePath = path.relative(packageDir, file).split(path.sep).join('/');
206
+ // Windows MAX_PATH checks should only be enforced on Windows hosts.
207
+ if (process.platform === 'win32' && file.length > 259) {
207
208
  hasLongPath = true;
209
+ excludedCount++;
208
210
  if (debug) {
209
211
  log(`Skipping long path: ${file}`, 'info');
210
212
  }
211
213
  continue;
212
214
  }
213
- const relativePath = path.relative(packageDir, file).split(path.sep).join('/');
214
215
  const isExcluded = (0, doExcludeFile_1.doExcludeFile)(relativePath);
215
216
  try {
216
217
  const stats = await (0, promises_1.stat)(file);
@@ -230,9 +231,8 @@ const processLayout = async (packageDir, options = {}) => {
230
231
  layout.content.push(content);
231
232
  }
232
233
  catch (error) {
233
- if (debug) {
234
- log(`Error processing file ${file}: ${error.message}`, 'info');
235
- }
234
+ const msg = `Error processing file ${file}: ${error.message}`;
235
+ log(msg, debug ? 'info' : 'warn');
236
236
  excludedCount++;
237
237
  }
238
238
  }
@@ -309,7 +309,7 @@ const processLayout = async (packageDir, options = {}) => {
309
309
  }
310
310
  };
311
311
  exports.processLayout = processLayout;
312
- const doProcessLayoutFile = (layoutPath) => (0, exports.processLayout)(layoutPath, { returnResult: false });
312
+ const doProcessLayoutFile = (packageDir, options = {}) => (0, exports.processLayout)(packageDir, { ...options, returnResult: false });
313
313
  exports.doProcessLayoutFile = doProcessLayoutFile;
314
314
  const doProcessLayoutFileCli = (packageDir, options = {}) => (0, exports.processLayout)(packageDir, { ...options, returnResult: true });
315
315
  exports.doProcessLayoutFileCli = doProcessLayoutFileCli;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "msfs-layout-generator",
3
- "version": "0.3.3",
3
+ "version": "0.3.5",
4
4
  "description": "Generate layout.json for MSFS community packages",
5
5
  "repository": {
6
6
  "type": "git",