Rork Project Code Exporter

Export all code files from Rork projects with folder structure

Size

12.2 KB

Version

1.3.6

Created

Oct 30, 2025

Updated

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