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})();