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