@evitcastudio/kit 3.2.1 → 3.3.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.
@@ -1,6 +1,7 @@
1
- import { promises as fs, watch as fsWatch, existsSync } from 'fs';
2
- import { join, extname, basename, resolve } from 'path';
1
+ import { promises as fs, existsSync } from 'fs';
2
+ import { join, extname, basename, resolve, relative } from 'path';
3
3
  import chalk from 'chalk';
4
+ import { watch as chokidarWatch } from 'chokidar';
4
5
  import { v4 as uuidv4 } from 'uuid';
5
6
  import { VYI } from '../vendor/vyi';
6
7
  import { bundleApp } from './app-bundler';
@@ -100,9 +101,22 @@ async function processDirectory(pDirectoryPath) {
100
101
  function isValidExtension(pExtension) {
101
102
  return VALID_EXTENSIONS.includes(pExtension);
102
103
  }
104
+ // Engine resource formats handled by Vylocity obfuscation
105
+ const ENGINE_EXTENSIONS = ['vyint', 'vyi', 'vym', 'vymac'];
106
+ /**
107
+ * Checks if a file extension is a Vylocity engine binary/definition format.
108
+ * @param pExtension - The file extension to test.
109
+ * @returns True if the extension is an engine format.
110
+ */
111
+ function isEngineExtension(pExtension) {
112
+ return ENGINE_EXTENSIONS.includes(pExtension);
113
+ }
103
114
  /**
104
115
  * Recursively copies a directory to destination, preserving subdirectories and files.
105
- * Ignores engine resource files that are already handled by Vylocity obfuscation.
116
+ * Ignores engine resource files (vyint, vyi, vym, vymac) that are handled by Vylocity obfuscation,
117
+ * while ensuring media and custom assets (sounds, images, fonts, json) retain their structure.
118
+ * @param pSourceDir - The source directory to copy.
119
+ * @param pDestDir - The target destination directory.
106
120
  */
107
121
  async function mirrorDirectory(pSourceDir, pDestDir) {
108
122
  const entries = await fs.readdir(pSourceDir, { withFileTypes: true });
@@ -115,9 +129,10 @@ async function mirrorDirectory(pSourceDir, pDestDir) {
115
129
  }
116
130
  else {
117
131
  const ext = extname(entry.name).slice(1);
118
- // Only copy non-engine files (images, custom JSON, fonts, etc.)
119
- if (!isValidExtension(ext)) {
120
- await fs.copyFile(srcPath, destPath);
132
+ // Copy all assets except internal Vylocity engine binary formats
133
+ if (!isEngineExtension(ext)) {
134
+ const data = await fs.readFile(srcPath);
135
+ await fs.writeFile(destPath, data);
121
136
  }
122
137
  }
123
138
  }
@@ -361,7 +376,11 @@ async function clearResourceTypeDirectories(pBaseDirectory) {
361
376
  async function copyFile(pSource, pDestinationDir, pNewName) {
362
377
  try {
363
378
  await fs.mkdir(pDestinationDir, { recursive: true });
364
- await fs.copyFile(pSource, join(pDestinationDir, pNewName));
379
+ // Use readFile + writeFile instead of fs.copyFile to avoid macOS APFS
380
+ // clone operations, which emit phantom FSEvents on the source file and
381
+ // cause external editors (e.g. Viewer) to falsely detect modifications.
382
+ const data = await fs.readFile(pSource);
383
+ await fs.writeFile(join(pDestinationDir, pNewName), data);
365
384
  }
366
385
  catch (pError) {
367
386
  logError(`[Error] Copying file ${pSource}: ${pError}`);
@@ -417,39 +436,139 @@ async function runBuild() {
417
436
  */
418
437
  async function runWatch() {
419
438
  await runBuild();
420
- console.log(chalk.cyan(`\nšŸ‘€ Watching for changes in: ${chalk.bold(resourceInDirectory)}`));
439
+ const pathsToWatch = [];
440
+ if (existsSync(resourceInDirectory)) {
441
+ pathsToWatch.push(resourceInDirectory);
442
+ }
443
+ const srcDir = join(projectRootDirectory, 'src');
444
+ if (shouldBundleApp && existsSync(srcDir) && srcDir !== resourceInDirectory) {
445
+ pathsToWatch.push(srcDir);
446
+ }
447
+ const displayPaths = pathsToWatch
448
+ .map(p => chalk.bold(relative(projectRootDirectory, p) || p))
449
+ .join(', ');
450
+ console.log(chalk.cyan(`\nWatching for changes in: ${displayPaths}`));
421
451
  let debounceTimer = null;
452
+ let isRebuilding = false;
453
+ let queuedChange = null;
422
454
  const triggerRebuild = (pFilename) => {
423
455
  if (debounceTimer)
424
456
  clearTimeout(debounceTimer);
425
457
  debounceTimer = setTimeout(async () => {
426
- console.log(chalk.dim(`\nFile changed: ${pFilename}, rebuilding...`));
427
- await runBuild();
458
+ if (isRebuilding) {
459
+ queuedChange = pFilename;
460
+ return;
461
+ }
462
+ isRebuilding = true;
463
+ try {
464
+ console.log(chalk.dim(`\nFile changed: ${pFilename}, rebuilding...`));
465
+ await runBuild();
466
+ }
467
+ finally {
468
+ isRebuilding = false;
469
+ if (queuedChange) {
470
+ const next = queuedChange;
471
+ queuedChange = null;
472
+ triggerRebuild(next);
473
+ }
474
+ }
428
475
  }, 150);
429
476
  };
430
- const watchers = [];
431
- if (existsSync(resourceInDirectory)) {
432
- const resWatcher = fsWatch(resourceInDirectory, { recursive: true }, (_eventType, pFilename) => {
433
- if (pFilename)
434
- triggerRebuild(pFilename);
435
- });
436
- watchers.push(resWatcher);
437
- }
438
- const srcDir = join(projectRootDirectory, 'src');
439
- if (shouldBundleApp && existsSync(srcDir) && srcDir !== resourceInDirectory) {
440
- const srcWatcher = fsWatch(srcDir, { recursive: true }, (_eventType, pFilename) => {
441
- if (!pFilename)
442
- return;
443
- // Ignore resources folder if inside src to prevent double triggers
444
- if (pFilename.startsWith('resources'))
445
- return;
446
- triggerRebuild(pFilename);
447
- });
448
- watchers.push(srcWatcher);
477
+ const normalizedOutDir = resourceOutDirectory ? resourceOutDirectory.replace(/\\/g, '/') : '';
478
+ const isIgnored = (pPath) => {
479
+ const normalized = pPath.replace(/\\/g, '/');
480
+ // Ignore output directory
481
+ if (normalizedOutDir && (normalized === normalizedOutDir || normalized.startsWith(`${normalizedOutDir}/`))) {
482
+ return true;
483
+ }
484
+ // Ignore VCS and dependency folders
485
+ if (/(^|[/\\])(\.git|node_modules|\.DS_Store|Thumbs\.db)($|[/\\])/.test(normalized)) {
486
+ return true;
487
+ }
488
+ // Ignore vendor directory (precompiled/static vendor assets)
489
+ if (/(^|[/\\])vendor([/\\]|$)/.test(normalized)) {
490
+ return true;
491
+ }
492
+ // Ignore sourcemaps
493
+ if (normalized.endsWith('.map')) {
494
+ return true;
495
+ }
496
+ // Ignore generated manifest metadata files in project root
497
+ if (normalized.endsWith('resource.json') ||
498
+ normalized.endsWith('bounds.json') ||
499
+ normalized.endsWith('icon-points.json') ||
500
+ normalized.endsWith('sizes.json')) {
501
+ return true;
502
+ }
503
+ return false;
504
+ };
505
+ // Track known file modification times and sizes to eliminate phantom change events from copy/read operations
506
+ const fileStats = new Map();
507
+ const recordFileStat = async (pFilePath) => {
508
+ try {
509
+ const stats = await fs.stat(pFilePath);
510
+ if (stats.isFile()) {
511
+ fileStats.set(resolve(pFilePath), { mtime: stats.mtimeMs, size: stats.size });
512
+ }
513
+ }
514
+ catch {
515
+ // Ignored
516
+ }
517
+ };
518
+ const primeDirectoryStats = async (pDirPath) => {
519
+ try {
520
+ const entries = await fs.readdir(pDirPath, { withFileTypes: true });
521
+ for (const entry of entries) {
522
+ const fullPath = join(pDirPath, entry.name);
523
+ if (isIgnored(fullPath))
524
+ continue;
525
+ if (entry.isDirectory()) {
526
+ await primeDirectoryStats(fullPath);
527
+ }
528
+ else if (entry.isFile()) {
529
+ await recordFileStat(fullPath);
530
+ }
531
+ }
532
+ }
533
+ catch {
534
+ // Ignored
535
+ }
536
+ };
537
+ for (const p of pathsToWatch) {
538
+ await primeDirectoryStats(p);
449
539
  }
450
- process.on('SIGINT', () => {
451
- for (const w of watchers)
452
- w.close();
540
+ const watcher = chokidarWatch(pathsToWatch, {
541
+ ignored: isIgnored,
542
+ ignoreInitial: true,
543
+ awaitWriteFinish: {
544
+ stabilityThreshold: 100,
545
+ pollInterval: 50
546
+ }
547
+ });
548
+ watcher.on('all', async (event, filePath) => {
549
+ if (event === 'addDir' || event === 'unlinkDir')
550
+ return;
551
+ const absPath = resolve(filePath);
552
+ if (event === 'unlink') {
553
+ fileStats.delete(absPath);
554
+ const relativePath = relative(projectRootDirectory, filePath).replace(/\\/g, '/');
555
+ triggerRebuild(relativePath);
556
+ return;
557
+ }
558
+ // Verify mtime or size actually changed to eliminate phantom OS events
559
+ const stats = await fs.stat(absPath).catch(() => null);
560
+ if (!stats)
561
+ return;
562
+ const prev = fileStats.get(absPath);
563
+ if (prev && prev.mtime === stats.mtimeMs && prev.size === stats.size) {
564
+ return;
565
+ }
566
+ fileStats.set(absPath, { mtime: stats.mtimeMs, size: stats.size });
567
+ const relativePath = relative(projectRootDirectory, filePath).replace(/\\/g, '/');
568
+ triggerRebuild(relativePath);
569
+ });
570
+ process.on('SIGINT', async () => {
571
+ await watcher.close();
453
572
  process.exit(0);
454
573
  });
455
574
  }
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env node
2
+ const { spawnSync } = require('child_process');
3
+ const path = require('path');
4
+
5
+ // Target the compiled Bun bundle
6
+ const cliScript = path.join(__dirname, 'bundle', 'cli', 'cli.js');
7
+
8
+ // If currently running under Bun, import and execute directly
9
+ if (typeof Bun !== 'undefined') {
10
+ import(cliScript);
11
+ } else {
12
+ // Running under Node.js -> delegate to Bun runtime
13
+ const result = spawnSync('bun', [cliScript, ...process.argv.slice(2)], {
14
+ stdio: 'inherit',
15
+ shell: process.platform === 'win32'
16
+ });
17
+ process.exit(result.status ?? 0);
18
+ }