@pixelbyte-software/pixcode 1.53.1 → 1.53.2
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/assets/{index-BO0lz8ZD.js → index-CP2HfVKD.js} +150 -132
- package/dist/assets/index-FhOsv2Yy.css +32 -0
- package/dist/index.html +2 -2
- package/dist-server/server/index.js +140 -9
- package/dist-server/server/index.js.map +1 -1
- package/package.json +1 -1
- package/server/index.js +159 -9
- package/dist/assets/index-DnjqWG8B.css +0 -32
|
@@ -624,7 +624,94 @@ async function setupProjectsWatcher() {
|
|
|
624
624
|
// `project_files_updated` events to subscribed clients only, letting the
|
|
625
625
|
// explorer refresh automatically without HTTP polling.
|
|
626
626
|
const WORKSPACE_WATCHER_DEBOUNCE_MS = 800;
|
|
627
|
-
const
|
|
627
|
+
const WORKSPACE_DIFF_SNAPSHOT_MAX_BYTES = 512 * 1024;
|
|
628
|
+
const WORKSPACE_DIFF_SNAPSHOT_MAX_FILES = 800;
|
|
629
|
+
const workspaceWatchers = new Map(); // projectName -> { watcher, subscribers, debounceTimer, pendingEvent, rootPath, fileSnapshots }
|
|
630
|
+
function runWorkspaceCommand(command, args, cwd) {
|
|
631
|
+
return new Promise((resolve, reject) => {
|
|
632
|
+
const child = spawn(command, args, {
|
|
633
|
+
cwd,
|
|
634
|
+
shell: false,
|
|
635
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
636
|
+
});
|
|
637
|
+
let stdout = '';
|
|
638
|
+
let stderr = '';
|
|
639
|
+
child.stdout?.on('data', (chunk) => {
|
|
640
|
+
stdout += chunk.toString();
|
|
641
|
+
});
|
|
642
|
+
child.stderr?.on('data', (chunk) => {
|
|
643
|
+
stderr += chunk.toString();
|
|
644
|
+
});
|
|
645
|
+
child.on('error', reject);
|
|
646
|
+
child.on('close', (code) => {
|
|
647
|
+
if (code === 0) {
|
|
648
|
+
resolve({ stdout, stderr });
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
reject(new Error(stderr || `${command} exited with code ${code}`));
|
|
652
|
+
});
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
function parseGitPorcelainZ(output) {
|
|
656
|
+
const entries = [];
|
|
657
|
+
const parts = String(output || '').split('\0').filter(Boolean);
|
|
658
|
+
for (let index = 0; index < parts.length; index += 1) {
|
|
659
|
+
const entry = parts[index];
|
|
660
|
+
if (entry.length < 4) {
|
|
661
|
+
continue;
|
|
662
|
+
}
|
|
663
|
+
const status = entry.slice(0, 2);
|
|
664
|
+
const filePath = entry.slice(3).replace(/\\/g, '/');
|
|
665
|
+
if (filePath) {
|
|
666
|
+
entries.push(filePath);
|
|
667
|
+
}
|
|
668
|
+
if (status[0] === 'R' || status[0] === 'C') {
|
|
669
|
+
index += 1;
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
return entries;
|
|
673
|
+
}
|
|
674
|
+
function normalizeWorkspaceRelativePath(rootPath, filePath) {
|
|
675
|
+
const relativePath = path.relative(rootPath, filePath).replace(/\\/g, '/');
|
|
676
|
+
if (!relativePath || relativePath.startsWith('../') || relativePath === '..' || path.isAbsolute(relativePath)) {
|
|
677
|
+
return null;
|
|
678
|
+
}
|
|
679
|
+
return relativePath;
|
|
680
|
+
}
|
|
681
|
+
async function readWorkspaceTextSnapshot(filePath) {
|
|
682
|
+
try {
|
|
683
|
+
const stats = await fsPromises.stat(filePath);
|
|
684
|
+
if (!stats.isFile() || stats.size > WORKSPACE_DIFF_SNAPSHOT_MAX_BYTES) {
|
|
685
|
+
return null;
|
|
686
|
+
}
|
|
687
|
+
const buffer = await fsPromises.readFile(filePath);
|
|
688
|
+
if (buffer.includes(0)) {
|
|
689
|
+
return null;
|
|
690
|
+
}
|
|
691
|
+
return buffer.toString('utf8');
|
|
692
|
+
}
|
|
693
|
+
catch (error) {
|
|
694
|
+
if (error?.code === 'ENOENT') {
|
|
695
|
+
return '';
|
|
696
|
+
}
|
|
697
|
+
return null;
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
async function initializeWorkspaceDirtySnapshots(rootPath, fileSnapshots) {
|
|
701
|
+
try {
|
|
702
|
+
const { stdout } = await runWorkspaceCommand('git', ['status', '--porcelain', '-z'], rootPath);
|
|
703
|
+
const changedPaths = parseGitPorcelainZ(stdout).slice(0, WORKSPACE_DIFF_SNAPSHOT_MAX_FILES);
|
|
704
|
+
await Promise.all(changedPaths.map(async (relativePath) => {
|
|
705
|
+
const content = await readWorkspaceTextSnapshot(path.join(rootPath, relativePath));
|
|
706
|
+
if (typeof content === 'string') {
|
|
707
|
+
fileSnapshots.set(relativePath, content);
|
|
708
|
+
}
|
|
709
|
+
}));
|
|
710
|
+
}
|
|
711
|
+
catch {
|
|
712
|
+
// Non-git workspaces still get event-time snapshots after the first change.
|
|
713
|
+
}
|
|
714
|
+
}
|
|
628
715
|
async function subscribeToWorkspace(ws, projectName) {
|
|
629
716
|
if (!projectName || typeof projectName !== 'string')
|
|
630
717
|
return;
|
|
@@ -650,10 +737,44 @@ async function subscribeToWorkspace(ws, projectName) {
|
|
|
650
737
|
debounceTimer: null,
|
|
651
738
|
pendingEvent: null,
|
|
652
739
|
rootPath,
|
|
740
|
+
fileSnapshots: new Map(),
|
|
653
741
|
};
|
|
654
742
|
workspaceWatchers.set(projectName, entry);
|
|
655
|
-
|
|
656
|
-
|
|
743
|
+
await initializeWorkspaceDirtySnapshots(rootPath, entry.fileSnapshots);
|
|
744
|
+
const broadcastFileUpdate = async (eventType, filePath) => {
|
|
745
|
+
const relativePath = filePath ? normalizeWorkspaceRelativePath(rootPath, filePath) : null;
|
|
746
|
+
const previousContent = relativePath ? entry.fileSnapshots.get(relativePath) : undefined;
|
|
747
|
+
let currentContent;
|
|
748
|
+
if (relativePath && !eventType.endsWith('Dir')) {
|
|
749
|
+
if (eventType === 'unlink') {
|
|
750
|
+
currentContent = '';
|
|
751
|
+
entry.fileSnapshots.delete(relativePath);
|
|
752
|
+
}
|
|
753
|
+
else {
|
|
754
|
+
const snapshot = await readWorkspaceTextSnapshot(filePath);
|
|
755
|
+
if (typeof snapshot === 'string') {
|
|
756
|
+
currentContent = snapshot;
|
|
757
|
+
entry.fileSnapshots.set(relativePath, snapshot);
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
const nextEvent = {
|
|
762
|
+
eventType,
|
|
763
|
+
filePath,
|
|
764
|
+
changedFile: relativePath,
|
|
765
|
+
oldContent: typeof previousContent === 'string'
|
|
766
|
+
? previousContent
|
|
767
|
+
: eventType === 'add'
|
|
768
|
+
? ''
|
|
769
|
+
: undefined,
|
|
770
|
+
currentContent,
|
|
771
|
+
};
|
|
772
|
+
entry.pendingEvent = entry.pendingEvent?.changedFile === relativePath
|
|
773
|
+
? {
|
|
774
|
+
...nextEvent,
|
|
775
|
+
oldContent: entry.pendingEvent.oldContent ?? nextEvent.oldContent,
|
|
776
|
+
}
|
|
777
|
+
: nextEvent;
|
|
657
778
|
if (entry.debounceTimer) {
|
|
658
779
|
clearTimeout(entry.debounceTimer);
|
|
659
780
|
}
|
|
@@ -661,11 +782,21 @@ async function subscribeToWorkspace(ws, projectName) {
|
|
|
661
782
|
entry.debounceTimer = null;
|
|
662
783
|
const pending = entry.pendingEvent || {};
|
|
663
784
|
entry.pendingEvent = null;
|
|
785
|
+
const hasSnapshotDiff = (typeof pending.oldContent === 'string'
|
|
786
|
+
&& typeof pending.currentContent === 'string'
|
|
787
|
+
&& pending.oldContent !== pending.currentContent);
|
|
664
788
|
const message = JSON.stringify({
|
|
665
789
|
type: 'project_files_updated',
|
|
666
790
|
projectName,
|
|
667
791
|
changeType: pending.eventType || 'change',
|
|
668
|
-
changedFile: pending.filePath ? path.relative(rootPath, pending.filePath) : null,
|
|
792
|
+
changedFile: pending.changedFile ?? (pending.filePath ? path.relative(rootPath, pending.filePath) : null),
|
|
793
|
+
...(hasSnapshotDiff
|
|
794
|
+
? {
|
|
795
|
+
oldContent: pending.oldContent,
|
|
796
|
+
currentContent: pending.currentContent,
|
|
797
|
+
diffSource: 'workspace-snapshot',
|
|
798
|
+
}
|
|
799
|
+
: {}),
|
|
669
800
|
timestamp: new Date().toISOString(),
|
|
670
801
|
});
|
|
671
802
|
entry.subscribers.forEach((client) => {
|
|
@@ -689,11 +820,11 @@ async function subscribeToWorkspace(ws, projectName) {
|
|
|
689
820
|
}
|
|
690
821
|
});
|
|
691
822
|
watcher
|
|
692
|
-
.on('add', (filePath) => broadcastFileUpdate('add', filePath))
|
|
693
|
-
.on('change', (filePath) => broadcastFileUpdate('change', filePath))
|
|
694
|
-
.on('unlink', (filePath) => broadcastFileUpdate('unlink', filePath))
|
|
695
|
-
.on('addDir', (dirPath) => broadcastFileUpdate('addDir', dirPath))
|
|
696
|
-
.on('unlinkDir', (dirPath) => broadcastFileUpdate('unlinkDir', dirPath))
|
|
823
|
+
.on('add', (filePath) => void broadcastFileUpdate('add', filePath))
|
|
824
|
+
.on('change', (filePath) => void broadcastFileUpdate('change', filePath))
|
|
825
|
+
.on('unlink', (filePath) => void broadcastFileUpdate('unlink', filePath))
|
|
826
|
+
.on('addDir', (dirPath) => void broadcastFileUpdate('addDir', dirPath))
|
|
827
|
+
.on('unlinkDir', (dirPath) => void broadcastFileUpdate('unlinkDir', dirPath))
|
|
697
828
|
.on('error', (error) => {
|
|
698
829
|
console.error(`[ERROR] Workspace watcher error for ${projectName}:`, error);
|
|
699
830
|
});
|