Rork Project Code Exporter

Export all code files from Rork projects with folder structure

Size

12.5 KB

Version

1.3.8

Created

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