Rork Project Code Exporter

Export all code files from Rork projects with folder structure

Size

11.5 KB

Version

1.3.10

Created

Mar 4, 2026

Updated

about 1 month ago

1// ==UserScript==
2// @name		Rork Project Code Exporter
3// @description		Export all code files from Rork projects with folder structure
4// @version		1.3.10
5// @match		https://*.rork.com/*
6// @icon		https://rork.com/favicon.ico
7// @require		https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js
8// ==/UserScript==
9(function() {
10    'use strict';
11
12    console.log('Rork Project Code Exporter loaded');
13
14    // Function to extract all file paths from the page
15    function extractFilePaths() {
16        const files = [];
17        
18        // NEW: Get files from the chat/history view (span elements with file paths)
19        const fileSpans = document.querySelectorAll('span.py-0\\.5.px-1');
20        fileSpans.forEach(el => {
21            const text = el.textContent.trim();
22            // Match file patterns: has extension or starts with dot
23            if (text && (text.match(/\.[a-z0-9]+$/i) || text.startsWith('.'))) {
24                if (!files.includes(text)) {
25                    files.push(text);
26                }
27            }
28        });
29        
30        console.log('Found files:', files);
31        return files;
32    }
33
34    // Function to click on a file and wait for content to load
35    async function clickFileAndGetContent(filePath) {
36        console.log('Clicking on file:', filePath);
37        
38        // Clear clipboard before starting
39        try {
40            await navigator.clipboard.writeText('');
41        } catch {
42            console.log('Could not clear clipboard');
43        }
44        
45        let fileElement = null;
46        
47        // First try to find in span elements (files with paths)
48        const spanElements = document.querySelectorAll('span.py-0\\.5.px-1');
49        for (const el of spanElements) {
50            if (el.textContent.trim() === filePath) {
51                fileElement = el;
52                break;
53            }
54        }
55        
56        // If not found, try button elements (root files)
57        if (!fileElement) {
58            const buttonElements = document.querySelectorAll('button.flex.items-center.gap-1.w-full.text-sm');
59            for (const el of buttonElements) {
60                if (el.textContent.trim() === filePath) {
61                    fileElement = el;
62                    break;
63                }
64            }
65        }
66        
67        if (!fileElement) {
68            console.error('File element not found:', filePath);
69            return null;
70        }
71        
72        // Click the file
73        fileElement.click();
74        
75        // Wait for content to load - increased wait time
76        await new Promise(resolve => setTimeout(resolve, 5000));
77        
78        // Find and click the Copy button - wait for it to appear
79        let copyButton = null;
80        let attempts = 0;
81        while (!copyButton && attempts < 10) {
82            const buttons = document.querySelectorAll('button');
83            for (const btn of buttons) {
84                if (btn.textContent.trim() === 'Copy') {
85                    copyButton = btn;
86                    break;
87                }
88            }
89            if (!copyButton) {
90                await new Promise(resolve => setTimeout(resolve, 500));
91                attempts++;
92            }
93        }
94        
95        if (!copyButton) {
96            console.error('Copy button not found for:', filePath);
97            
98            // Fallback: Try to get content directly from the editor
99            console.log('Attempting fallback method for:', filePath);
100            const cmContent = document.querySelector('.cm-content');
101            if (cmContent && cmContent.innerText) {
102                const content = cmContent.innerText;
103                console.log(`Fallback successful for ${filePath}, length: ${content.length} chars`);
104                return content;
105            }
106            return null;
107        }
108        
109        // Click the copy button
110        copyButton.click();
111        
112        // Wait longer for clipboard to be populated
113        await new Promise(resolve => setTimeout(resolve, 1000));
114        
115        // Read from clipboard with verification
116        let content = null;
117        let clipboardAttempts = 0;
118        while (!content && clipboardAttempts < 3) {
119            try {
120                content = await navigator.clipboard.readText();
121                if (content && content.length > 0) {
122                    console.log(`Found content for ${filePath}, length: ${content.length} chars`);
123                    break;
124                } else {
125                    // Empty content, wait and retry
126                    await new Promise(resolve => setTimeout(resolve, 500));
127                    clipboardAttempts++;
128                }
129            } catch (error) {
130                console.error('Failed to read clipboard for:', filePath, error);
131                clipboardAttempts++;
132                await new Promise(resolve => setTimeout(resolve, 500));
133            }
134        }
135        
136        // If clipboard failed, use fallback
137        if (!content || content.length === 0) {
138            console.log('Attempting fallback method for:', filePath);
139            const cmContent = document.querySelector('.cm-content');
140            if (cmContent && cmContent.innerText) {
141                content = cmContent.innerText;
142                console.log(`Fallback successful for ${filePath}, length: ${content.length} chars`);
143            }
144        }
145        
146        return content;
147    }
148
149    // Function to create downloadable ZIP export
150    async function createZipExport(filesData) {
151        const zip = new JSZip();
152        
153        // Add each file to the ZIP with its full path
154        filesData.forEach(({ path, content }) => {
155            zip.file(path, content || '// No content available');
156        });
157        
158        // Generate the ZIP file
159        const zipBlob = await zip.generateAsync({ type: 'blob' });
160        return zipBlob;
161    }
162
163    // Function to download the ZIP export
164    function downloadZipExport(blob, filename) {
165        const url = URL.createObjectURL(blob);
166        const a = document.createElement('a');
167        a.href = url;
168        a.download = filename;
169        document.body.appendChild(a);
170        a.click();
171        document.body.removeChild(a);
172        URL.revokeObjectURL(url);
173    }
174
175    // Main export function
176    async function exportAllCode() {
177        try {
178            console.log('Starting code export...');
179            
180            const exportButton = document.getElementById('rork-export-btn');
181            if (exportButton) {
182                exportButton.textContent = '⏳ Exporting...';
183                exportButton.disabled = true;
184            }
185            
186            // Extract all file paths
187            const filePaths = extractFilePaths();
188            
189            if (filePaths.length === 0) {
190                alert('No code files found in this project!');
191                return;
192            }
193            
194            console.log(`Found ${filePaths.length} files to export`);
195            
196            // Collect content from all files
197            const filesData = [];
198            
199            for (let i = 0; i < filePaths.length; i++) {
200                const filePath = filePaths[i];
201                console.log(`Processing file ${i + 1}/${filePaths.length}: ${filePath}`);
202                
203                if (exportButton) {
204                    exportButton.textContent = `${i + 1}/${filePaths.length}`;
205                }
206                
207                const content = await clickFileAndGetContent(filePath);
208                
209                filesData.push({
210                    path: filePath,
211                    content: content
212                });
213            }
214            
215            // Create ZIP file
216            if (exportButton) {
217                exportButton.textContent = '⏳ Creating ZIP...';
218            }
219            const zipBlob = await createZipExport(filesData);
220            
221            // Get project name from page title
222            const projectName = document.title.split('|')[0].trim().replace(/[^a-z0-9]/gi, '_');
223            const filename = `${projectName}_${Date.now()}.zip`;
224            
225            // Download the ZIP
226            downloadZipExport(zipBlob, filename);
227            
228            console.log('Export completed successfully!');
229            alert(`✅ Successfully exported ${filePaths.length} files as ZIP!\n\nFile saved as: ${filename}`);
230            
231            if (exportButton) {
232                exportButton.textContent = '📥 Export All Code';
233                exportButton.disabled = false;
234            }
235            
236        } catch (error) {
237            console.error('Export failed:', error);
238            alert('❌ Export failed: ' + error.message);
239            
240            const exportButton = document.getElementById('rork-export-btn');
241            if (exportButton) {
242                exportButton.textContent = '📥 Export All Code';
243                exportButton.disabled = false;
244            }
245        }
246    }
247
248    // Function to add export button to the page
249    function addExportButton() {
250        // Check if button already exists
251        if (document.getElementById('rork-export-btn')) {
252            return;
253        }
254        
255        // Find the header area to add the button - updated selector for current Rork structure
256        const header = document.querySelector('header.flex.h-\\[40px\\]') || document.querySelector('header.flex.h-\\[53px\\]') || document.querySelector('header.flex');
257        
258        if (!header) {
259            console.log('Header not found, will retry...');
260            return;
261        }
262        
263        // Create export button with styling matching other header buttons
264        const exportButton = document.createElement('button');
265        exportButton.id = 'rork-export-btn';
266        exportButton.className = 'relative inline-flex items-center justify-center whitespace-nowrap text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 bg-secondary/70 text-secondary-foreground hover:bg-secondary/80 h-9 rounded-md px-3';
267        
268        // Create button content with icon and text
269        const buttonContent = document.createElement('div');
270        buttonContent.className = 'flex w-full flex-row items-center justify-center gap-2';
271        buttonContent.innerHTML = '<span>📥</span><span>Export All Code</span>';
272        
273        exportButton.appendChild(buttonContent);
274        exportButton.onclick = exportAllCode;
275        
276        // Insert button into header - find the right position
277        const flexEnd = header.querySelector('.flex-1');
278        if (flexEnd && flexEnd.nextSibling) {
279            header.insertBefore(exportButton, flexEnd.nextSibling);
280        } else {
281            header.appendChild(exportButton);
282        }
283        
284        console.log('Export button added successfully');
285    }
286
287    // Initialize the extension
288    function init() {
289        console.log('Initializing Rork Project Code Exporter...');
290        
291        // Wait for page to load
292        if (document.readyState === 'loading') {
293            document.addEventListener('DOMContentLoaded', init);
294            return;
295        }
296        
297        // Add button after a short delay to ensure page is ready
298        setTimeout(() => {
299            addExportButton();
300            
301            // Also observe for dynamic content changes
302            const observer = new MutationObserver(() => {
303                addExportButton();
304            });
305            
306            observer.observe(document.body, {
307                childList: true,
308                subtree: true
309            });
310        }, 2000);
311    }
312
313    // Start the extension
314    init();
315
316})();
Rork Project Code Exporter | Robomonkey