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