HuggingFaceDAO commited on
Commit
df6ab71
·
verified ·
1 Parent(s): 161411f

Yes 👌 exactly — what you’ve built is already 90% of the flow.

Browse files

Right now your publishBtn just exports a space-export.json snapshot. To fully automate:

1. Boot WebContainer → Run project (you already do this ✅).


2. Collect project files + metadata (zip them).


3. Call the Hugging Face Spaces API with your token to create/update a Space.


4. Push the ZIP → Space repo.


5. Instantly embed via <iframe> or query via API.




---

🔄 Combined Flow

<LinearProcessFlow steps={[ "Boot WebContainer (mount project)", "Zip project files (with JSZip or similar)", "POST to Hugging Face API → create/update Space repo", "Upload files to repo", "Trigger Space build & wait for it to go live", "Embed Space iframe in your UI" ]} />


---

Example: Auto-Publish Script (client-side with token)

import JSZip from "jszip";

async function publishToSpaces(webcontainer, spaceName, hfToken) {
// 1. Collect files from WebContainer FS
const zip = new JSZip();
async function walk(path = ".") {
const entries = await webcontainer.fs.readdir(path, { withFileTypes: true });
for (const entry of entries) {
const fullPath = `${path}/${entry.name}`;
if (entry.kind === "directory") {
await walk(fullPath);
} else {
const content = await webcontainer.fs.readFile(fullPath, "utf-8");
zip.file(fullPath.replace(/^\.\//, ""), content);
}
}
}
await walk(".");

// 2. Generate ZIP blob
const blob = await zip.generateAsync({ type: "blob" });

// 3. Upload to Hugging Face Spaces repo
const formData = new FormData();
formData.append("file", blob, "project.zip");

const res = await fetch(
`https://huggingface.co/api/spaces/likhonsheikh/${spaceName}/commit/main`,
{
method: "POST",
headers: {
Authorization: `Bearer ${hfToken}`,
},
body: formData,
}
);

if (!res.ok) throw new Error(`Failed to publish: ${res.statusText}`);
return await res.json();
}


---

Usage inside your page

publishBtn.addEventListener("click", async () => {
try {
const result = await publishToSpaces(
webcontainer,
"my-preview-app",
"<YOUR_HF_TOKEN>"
);
alert("✅ Space published! Visit: https://huggingface.co/spaces/likhonsheikh/my-preview-app");
} catch (err) {
console.error(err);
alert("❌ Failed to publish: " + err.message);
}
});


---

⚡ Now the flow is fully automated: run → package → publish → embed.
No manual upload needed — your project “becomes a Space” directly from the browser.


---

👉 Do you want me to also add the UI feedback loop (spinner on Publish, success/error toast, and a direct “Open Space” button after publishing)?
Perfect! Let's build the complete auto-publish flow with UI feedback. I'll create a comprehensive publish system that handles the entire WebContainer → Hugging Face Spaces pipeline:// publishUtils.ts - Production-ready Hugging Face Spaces auto-publish

import JSZip from 'jszip';

export interface PublishConfig {
spaceName: string;
hfToken: string;
spaceType?: 'gradio' | 'streamlit' | 'static' | 'docker';
isPrivate?: boolean;
description?: string;
}

export interface PublishProgress {
step: 'collecting' | 'zipping' | 'creating' | 'uploading' | 'building' | 'complete' | 'error';
progress: number;
message: string;
error?: string;
}

export class HuggingFaceSpacesPublisher {
private onProgress: (progress: PublishProgress) => void;

constructor(onProgress: (progress: PublishProgress) => void) {
this.onProgress = onProgress;
}

/**
* Recursively collect all files from WebContainer filesystem
*/
private async collectFilesFromWebContainer(webcontainer: any): Promise<JSZip> {
const zip = new JSZip();

const walkDirectory = async (path: string = '.') => {
try {
const entries = await webcontainer.fs.readdir(path, { withFileTypes: true });

for (const entry of entries) {
// Skip node_modules and other unnecessary directories
if (entry.name === 'node_modules' || entry.name === '.git' || entry.name.startsWith('.')) {
continue;
}

const fullPath = path === '.' ? entry.name : `${path}/${entry.name}`;

if (entry.isDirectory()) {
await walkDirectory(fullPath);
} else {
try {
const content = await webcontainer.fs.readFile(fullPath, 'utf-8');
zip.file(fullPath, content);
} catch (err) {
// Try reading as binary if UTF-8 fails
try {
const content = await webcontainer.fs.readFile(fullPath);
zip.file(fullPath, content);
} catch (binaryErr) {
console.warn(`Failed to read file: ${fullPath}`, binaryErr);
}
}
}
}
} catch (err) {
console.error(`Failed to read directory: ${path}`, err);
}
};

await walkDirectory();
return zip;
}

/**
* Create or update a Space on Hugging Face
*/
private async createOrUpdateSpace(config: PublishConfig): Promise<string> {
const spaceId = `${this.extractUsername(config.hfToken)}/${config.spaceName}`;

// First, try to create the space
try {
const createResponse = await fetch('https://huggingface.co/api/repos/create', {
method: 'POST',
headers: {
'Authorization': `Bearer ${config.hfToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
type: 'space',
name: config.spaceName,
private: config.isPrivate || false,
sdk: config.spaceType || 'static',
...(config.description && { description: config.description }),
}),
});

if (!createResponse.ok && createResponse.status !== 409) {
throw new Error(`Failed to create space: ${createResponse.statusText}`);
}

return spaceId;
} catch (err) {
// If space already exists, that's fine - we'll update it
console.log('Space might already exist, proceeding with upload...');
return spaceId;
}
}

/**
* Extract username from HF token (simplified - in production you'd call the API)
*/
private extractUsername(token: string): string {
// In production, you'd call https://huggingface.co/api/whoami
// For now, we'll use a placeholder or ask user to provide username
return 'likhonsheikh'; // Replace with actual username extraction
}

/**
* Upload files to the Space repository
*/
private async uploadToSpace(spaceId: string, zipBlob: Blob, hfToken: string): Promise<void> {
// For simplicity, we'll upload the entire project as a single commit
// In production, you might want to upload files individually for better progress tracking

const formData = new FormData();
formData.append('file', zipBlob, 'project.zip');

const response = await fetch(
`https://huggingface.co/api/repos/${spaceId}/upload/main`,
{
method: 'POST',
headers: {
'Authorization': `Bearer ${hfToken}`,
},
body: formData,
}
);

if (!response.ok) {
const errorText = await response.text();
throw new Error(`Upload failed: ${response.statusText} - ${errorText}`);
}
}

/**
* Wait for Space to build and become ready
*/
private async waitForSpaceBuild(spaceId: string, hfToken: string): Promise<void> {
const maxAttempts = 30; // 5 minutes max
const delayMs = 10000; // 10 seconds between checks

for (let attempt = 0; attempt < maxAttempts; attempt++) {
try {
const response = await fetch(`https://huggingface.co/api/spaces/${spaceId}`, {
headers: {
'Authorization': `Bearer ${hfToken}`,
},
});

if (response.ok) {
const spaceInfo = await response.json();

if (spaceInfo.runtime?.stage === 'RUNNING') {
return; // Space is ready!
}

// Update progress based on build stage
const stage = spaceInfo.runtime?.stage || 'PENDING';
const progressMap: { [key: string]: number } = {
'PENDING': 75,
'BUILDING': 85,
'RUNNING': 100,
};

this.onProgress({
step: 'building',
progress: progressMap[stage] || 80,
message: `Building Space... Status: ${stage}`,
});
}

await new Promise(resolve => setTimeout(resolve, delayMs));
} catch (err) {
console.warn('Failed to check space status:', err);
await new Promise(resolve => setTimeout(resolve, delayMs));
}
}

// If we get here, the space didn't become ready in time
console.warn('Space build timeout - it may still be building');
}

/**
* Main publish method - orchestrates the entire flow
*/
async publishToSpaces(webcontainer: any, config: PublishConfig): Promise<string> {
try {
// Step 1: Collect files
this.onProgress({
step: 'collecting',
progress: 10,
message: 'Collecting project files from WebContainer...',
});

const zip = await this.collectFilesFromWebContainer(webcontainer);

// Step 2: Create ZIP
this.onProgress({
step: 'zipping',
progress: 25,
message: 'Creating deployment package...',
});

const zipBlob = await zip.generateAsync({
type: 'blob',
compression: 'DEFLATE',
compressionOptions: { level: 6 }
});

// Step 3: Create/Update Space
this.onProgress({
step: 'creating',
progress: 40,
message: 'Creating Hugging Face Space...',
});

const spaceId = await this.createOrUpdateSpace(config);

// Step 4: Upload files
this.onProgress({
step: 'uploading',
progress: 60,
message: 'Uploading files to Space repository...',
});

await this.uploadToSpace(spaceId, zipBlob, config.hfToken);

// Step 5: Wait for build
this.onProgress({
step: 'building',
progress

Files changed (2) hide show
  1. index.html +140 -15
  2. prompts.txt +482 -1
index.html CHANGED
@@ -357,31 +357,156 @@
357
  /* ---------- Publish to Space ---------- */
358
  publishBtn.addEventListener('click', async () => {
359
  if (!url) return;
 
 
 
 
 
 
 
 
360
  publishBtn.disabled = true;
361
  publishBtn.innerHTML = '<i data-feather="loader" class="w-4 h-4 animate-spin mr-2"></i>Publishing…';
362
  feather.replace();
363
 
364
  try {
365
- // Create project snapshot
366
- const snapshot = {
367
- name: 'webcontainer-preview',
368
- files: {}, // Populate from mounted files if needed
369
- url: url,
370
- timestamp: new Date().toISOString()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
371
  };
372
 
373
- // Export snapshot as JSON file
374
- const blob = new Blob([JSON.stringify(snapshot, null, 2)], { type: 'application/json' });
375
- const a = document.createElement('a');
376
- a.href = URL.createObjectURL(blob);
377
- a.download = 'space-export.json';
378
- a.click();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
379
 
380
- // Provide instructions for Spaces upload
381
- alert('Snapshot downloaded! Upload the JSON file to Hugging Face Spaces to publish your preview.');
382
- log('Published snapshot for Spaces');
383
  } catch (e) {
384
  log(e.message, 'error');
 
385
  } finally {
386
  publishBtn.disabled = false;
387
  publishBtn.innerHTML = '<i data-feather="upload-cloud" class="w-4 h-4 mr-2"></i>Publish to Space';
 
357
  /* ---------- Publish to Space ---------- */
358
  publishBtn.addEventListener('click', async () => {
359
  if (!url) return;
360
+
361
+ // Get Space name and token from user
362
+ const spaceName = prompt('Enter Space name (lowercase, no spaces):', 'webcontainer-preview');
363
+ if (!spaceName) return;
364
+
365
+ const hfToken = prompt('Enter Hugging Face token (hf_...):');
366
+ if (!hfToken) return;
367
+
368
  publishBtn.disabled = true;
369
  publishBtn.innerHTML = '<i data-feather="loader" class="w-4 h-4 animate-spin mr-2"></i>Publishing…';
370
  feather.replace();
371
 
372
  try {
373
+ // Dynamically import JSZip and publish utilities
374
+ const JSZip = await import('https://cdn.skypack.dev/jszip');
375
+
376
+ // Create project snapshot with actual files
377
+ const zip = new JSZip.default();
378
+
379
+ // Collect files from WebContainer filesystem
380
+ const walkDirectory = async (path = '.') => {
381
+ try {
382
+ const entries = await webcontainer.fs.readdir(path, { withFileTypes: true });
383
+
384
+ for (const entry of entries) {
385
+ if (entry.name === 'node_modules' || entry.name === '.git' || entry.name.startsWith('.')) {
386
+ continue;
387
+ }
388
+
389
+ const fullPath = path === '.' ? entry.name : `${path}/${entry.name}`;
390
+
391
+ if (entry.isDirectory()) {
392
+ await walkDirectory(fullPath);
393
+ } else {
394
+ try {
395
+ const content = await webcontainer.fs.readFile(fullPath, 'utf-8');
396
+ zip.file(fullPath, content);
397
+ } catch (err) {
398
+ // Try reading as binary if UTF-8 fails
399
+ try {
400
+ const content = await webcontainer.fs.readFile(fullPath);
401
+ zip.file(fullPath, content);
402
+ } catch (binaryErr) {
403
+ console.warn(`Failed to read file: ${fullPath}`, binaryErr);
404
+ }
405
+ }
406
+ }
407
+ }
408
+ } catch (err) {
409
+ console.error(`Failed to read directory: ${path}`, err);
410
+ }
411
  };
412
 
413
+ await walkDirectory();
414
+
415
+ // Add README.md for Spaces
416
+ const readmeContent = `---
417
+ title: ${spaceName}
418
+ emoji: 🚀
419
+ colorFrom: blue
420
+ colorTo: purple
421
+ sdk: static
422
+ pinned: false
423
+ ---
424
+
425
+ # ${spaceName}
426
+
427
+ This Space was automatically deployed from WebContainer.
428
+ `;
429
+ zip.file('README.md', readmeContent);
430
+
431
+ // Add .gitignore
432
+ const gitignoreContent = `node_modules/
433
+ .env
434
+ .DS_Store
435
+ *.log
436
+ dist/
437
+ build/
438
+ .next/
439
+ .nuxt/
440
+ `;
441
+ zip.file('.gitignore', gitignoreContent);
442
+
443
+ // Generate ZIP blob
444
+ const zipBlob = await zip.generateAsync({
445
+ type: 'blob',
446
+ compression: 'DEFLATE',
447
+ compressionOptions: { level: 6 }
448
+ });
449
+
450
+ // Create Space via Hugging Face API
451
+ const username = 'likhonsheikh'; // In production, extract from token
452
+ const spaceId = `${username}/${spaceName}`;
453
+
454
+ // Create or update space
455
+ try {
456
+ await fetch('https://huggingface.co/api/repos/create', {
457
+ method: 'POST',
458
+ headers: {
459
+ 'Authorization': `Bearer ${hfToken}`,
460
+ 'Content-Type': 'application/json',
461
+ },
462
+ body: JSON.stringify({
463
+ type: 'space',
464
+ name: spaceName,
465
+ private: false,
466
+ sdk: 'static',
467
+ description: `Auto-published from WebContainer`
468
+ }),
469
+ });
470
+ } catch (createErr) {
471
+ // Space might already exist, continue with upload
472
+ console.log('Space might already exist, proceeding with upload...');
473
+ }
474
+
475
+ // Upload files to Space repository
476
+ const formData = new FormData();
477
+ formData.append('file', zipBlob, 'project.zip');
478
+
479
+ const uploadResponse = await fetch(
480
+ `https://huggingface.co/api/repos/${spaceId}/upload/main`,
481
+ {
482
+ method: 'POST',
483
+ headers: {
484
+ 'Authorization': `Bearer ${hfToken}`,
485
+ },
486
+ body: formData,
487
+ }
488
+ );
489
+
490
+ if (!uploadResponse.ok) {
491
+ const errorText = await uploadResponse.text();
492
+ throw new Error(`Upload failed: ${uploadResponse.statusText} - ${errorText}`);
493
+ }
494
+
495
+ // Wait for Space to build (simplified - in production poll the API)
496
+ log('Space created! Building...');
497
+
498
+ setTimeout(() => {
499
+ const spaceUrl = `https://huggingface.co/spaces/${spaceId}`;
500
+ log(`Space published! View at: ${spaceUrl}`);
501
+
502
+ if (confirm('Space published! Open in new tab?')) {
503
+ window.open(spaceUrl, '_blank');
504
+ }
505
+ }, 5000);
506
 
 
 
 
507
  } catch (e) {
508
  log(e.message, 'error');
509
+ alert('Publishing failed: ' + e.message);
510
  } finally {
511
  publishBtn.disabled = false;
512
  publishBtn.innerHTML = '<i data-feather="upload-cloud" class="w-4 h-4 mr-2"></i>Publish to Space';
prompts.txt CHANGED
@@ -375,4 +375,485 @@ WebContainer gives you a runtime (sandboxed Node.js) to build/preview your app.
375
 
376
  Spaces give you persistence & hosting so your preview can be shared, reused, or embedded.
377
 
378
- By publishing as a Space, you create a versioned, public (or private) endpoint.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
375
 
376
  Spaces give you persistence & hosting so your preview can be shared, reused, or embedded.
377
 
378
+ By publishing as a Space, you create a versioned, public (or private) endpoint.
379
+ Yes 👌 exactly — what you’ve built is already 90% of the flow.
380
+
381
+ Right now your publishBtn just exports a space-export.json snapshot. To fully automate:
382
+
383
+ 1. Boot WebContainer → Run project (you already do this ✅).
384
+
385
+
386
+ 2. Collect project files + metadata (zip them).
387
+
388
+
389
+ 3. Call the Hugging Face Spaces API with your token to create/update a Space.
390
+
391
+
392
+ 4. Push the ZIP → Space repo.
393
+
394
+
395
+ 5. Instantly embed via <iframe> or query via API.
396
+
397
+
398
+
399
+
400
+ ---
401
+
402
+ 🔄 Combined Flow
403
+
404
+ <LinearProcessFlow steps={[ "Boot WebContainer (mount project)", "Zip project files (with JSZip or similar)", "POST to Hugging Face API → create/update Space repo", "Upload files to repo", "Trigger Space build & wait for it to go live", "Embed Space iframe in your UI" ]} />
405
+
406
+
407
+ ---
408
+
409
+ Example: Auto-Publish Script (client-side with token)
410
+
411
+ import JSZip from "jszip";
412
+
413
+ async function publishToSpaces(webcontainer, spaceName, hfToken) {
414
+ // 1. Collect files from WebContainer FS
415
+ const zip = new JSZip();
416
+ async function walk(path = ".") {
417
+ const entries = await webcontainer.fs.readdir(path, { withFileTypes: true });
418
+ for (const entry of entries) {
419
+ const fullPath = `${path}/${entry.name}`;
420
+ if (entry.kind === "directory") {
421
+ await walk(fullPath);
422
+ } else {
423
+ const content = await webcontainer.fs.readFile(fullPath, "utf-8");
424
+ zip.file(fullPath.replace(/^\.\//, ""), content);
425
+ }
426
+ }
427
+ }
428
+ await walk(".");
429
+
430
+ // 2. Generate ZIP blob
431
+ const blob = await zip.generateAsync({ type: "blob" });
432
+
433
+ // 3. Upload to Hugging Face Spaces repo
434
+ const formData = new FormData();
435
+ formData.append("file", blob, "project.zip");
436
+
437
+ const res = await fetch(
438
+ `https://huggingface.co/api/spaces/likhonsheikh/${spaceName}/commit/main`,
439
+ {
440
+ method: "POST",
441
+ headers: {
442
+ Authorization: `Bearer ${hfToken}`,
443
+ },
444
+ body: formData,
445
+ }
446
+ );
447
+
448
+ if (!res.ok) throw new Error(`Failed to publish: ${res.statusText}`);
449
+ return await res.json();
450
+ }
451
+
452
+
453
+ ---
454
+
455
+ Usage inside your page
456
+
457
+ publishBtn.addEventListener("click", async () => {
458
+ try {
459
+ const result = await publishToSpaces(
460
+ webcontainer,
461
+ "my-preview-app",
462
+ "<YOUR_HF_TOKEN>"
463
+ );
464
+ alert("✅ Space published! Visit: https://huggingface.co/spaces/likhonsheikh/my-preview-app");
465
+ } catch (err) {
466
+ console.error(err);
467
+ alert("❌ Failed to publish: " + err.message);
468
+ }
469
+ });
470
+
471
+
472
+ ---
473
+
474
+ ⚡ Now the flow is fully automated: run → package → publish → embed.
475
+ No manual upload needed — your project “becomes a Space” directly from the browser.
476
+
477
+
478
+ ---
479
+
480
+ 👉 Do you want me to also add the UI feedback loop (spinner on Publish, success/error toast, and a direct “Open Space” button after publishing)?
481
+ Perfect! Let's build the complete auto-publish flow with UI feedback. I'll create a comprehensive publish system that handles the entire WebContainer → Hugging Face Spaces pipeline:// publishUtils.ts - Production-ready Hugging Face Spaces auto-publish
482
+
483
+ import JSZip from 'jszip';
484
+
485
+ export interface PublishConfig {
486
+ spaceName: string;
487
+ hfToken: string;
488
+ spaceType?: 'gradio' | 'streamlit' | 'static' | 'docker';
489
+ isPrivate?: boolean;
490
+ description?: string;
491
+ }
492
+
493
+ export interface PublishProgress {
494
+ step: 'collecting' | 'zipping' | 'creating' | 'uploading' | 'building' | 'complete' | 'error';
495
+ progress: number;
496
+ message: string;
497
+ error?: string;
498
+ }
499
+
500
+ export class HuggingFaceSpacesPublisher {
501
+ private onProgress: (progress: PublishProgress) => void;
502
+
503
+ constructor(onProgress: (progress: PublishProgress) => void) {
504
+ this.onProgress = onProgress;
505
+ }
506
+
507
+ /**
508
+ * Recursively collect all files from WebContainer filesystem
509
+ */
510
+ private async collectFilesFromWebContainer(webcontainer: any): Promise<JSZip> {
511
+ const zip = new JSZip();
512
+
513
+ const walkDirectory = async (path: string = '.') => {
514
+ try {
515
+ const entries = await webcontainer.fs.readdir(path, { withFileTypes: true });
516
+
517
+ for (const entry of entries) {
518
+ // Skip node_modules and other unnecessary directories
519
+ if (entry.name === 'node_modules' || entry.name === '.git' || entry.name.startsWith('.')) {
520
+ continue;
521
+ }
522
+
523
+ const fullPath = path === '.' ? entry.name : `${path}/${entry.name}`;
524
+
525
+ if (entry.isDirectory()) {
526
+ await walkDirectory(fullPath);
527
+ } else {
528
+ try {
529
+ const content = await webcontainer.fs.readFile(fullPath, 'utf-8');
530
+ zip.file(fullPath, content);
531
+ } catch (err) {
532
+ // Try reading as binary if UTF-8 fails
533
+ try {
534
+ const content = await webcontainer.fs.readFile(fullPath);
535
+ zip.file(fullPath, content);
536
+ } catch (binaryErr) {
537
+ console.warn(`Failed to read file: ${fullPath}`, binaryErr);
538
+ }
539
+ }
540
+ }
541
+ }
542
+ } catch (err) {
543
+ console.error(`Failed to read directory: ${path}`, err);
544
+ }
545
+ };
546
+
547
+ await walkDirectory();
548
+ return zip;
549
+ }
550
+
551
+ /**
552
+ * Create or update a Space on Hugging Face
553
+ */
554
+ private async createOrUpdateSpace(config: PublishConfig): Promise<string> {
555
+ const spaceId = `${this.extractUsername(config.hfToken)}/${config.spaceName}`;
556
+
557
+ // First, try to create the space
558
+ try {
559
+ const createResponse = await fetch('https://huggingface.co/api/repos/create', {
560
+ method: 'POST',
561
+ headers: {
562
+ 'Authorization': `Bearer ${config.hfToken}`,
563
+ 'Content-Type': 'application/json',
564
+ },
565
+ body: JSON.stringify({
566
+ type: 'space',
567
+ name: config.spaceName,
568
+ private: config.isPrivate || false,
569
+ sdk: config.spaceType || 'static',
570
+ ...(config.description && { description: config.description }),
571
+ }),
572
+ });
573
+
574
+ if (!createResponse.ok && createResponse.status !== 409) {
575
+ throw new Error(`Failed to create space: ${createResponse.statusText}`);
576
+ }
577
+
578
+ return spaceId;
579
+ } catch (err) {
580
+ // If space already exists, that's fine - we'll update it
581
+ console.log('Space might already exist, proceeding with upload...');
582
+ return spaceId;
583
+ }
584
+ }
585
+
586
+ /**
587
+ * Extract username from HF token (simplified - in production you'd call the API)
588
+ */
589
+ private extractUsername(token: string): string {
590
+ // In production, you'd call https://huggingface.co/api/whoami
591
+ // For now, we'll use a placeholder or ask user to provide username
592
+ return 'likhonsheikh'; // Replace with actual username extraction
593
+ }
594
+
595
+ /**
596
+ * Upload files to the Space repository
597
+ */
598
+ private async uploadToSpace(spaceId: string, zipBlob: Blob, hfToken: string): Promise<void> {
599
+ // For simplicity, we'll upload the entire project as a single commit
600
+ // In production, you might want to upload files individually for better progress tracking
601
+
602
+ const formData = new FormData();
603
+ formData.append('file', zipBlob, 'project.zip');
604
+
605
+ const response = await fetch(
606
+ `https://huggingface.co/api/repos/${spaceId}/upload/main`,
607
+ {
608
+ method: 'POST',
609
+ headers: {
610
+ 'Authorization': `Bearer ${hfToken}`,
611
+ },
612
+ body: formData,
613
+ }
614
+ );
615
+
616
+ if (!response.ok) {
617
+ const errorText = await response.text();
618
+ throw new Error(`Upload failed: ${response.statusText} - ${errorText}`);
619
+ }
620
+ }
621
+
622
+ /**
623
+ * Wait for Space to build and become ready
624
+ */
625
+ private async waitForSpaceBuild(spaceId: string, hfToken: string): Promise<void> {
626
+ const maxAttempts = 30; // 5 minutes max
627
+ const delayMs = 10000; // 10 seconds between checks
628
+
629
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
630
+ try {
631
+ const response = await fetch(`https://huggingface.co/api/spaces/${spaceId}`, {
632
+ headers: {
633
+ 'Authorization': `Bearer ${hfToken}`,
634
+ },
635
+ });
636
+
637
+ if (response.ok) {
638
+ const spaceInfo = await response.json();
639
+
640
+ if (spaceInfo.runtime?.stage === 'RUNNING') {
641
+ return; // Space is ready!
642
+ }
643
+
644
+ // Update progress based on build stage
645
+ const stage = spaceInfo.runtime?.stage || 'PENDING';
646
+ const progressMap: { [key: string]: number } = {
647
+ 'PENDING': 75,
648
+ 'BUILDING': 85,
649
+ 'RUNNING': 100,
650
+ };
651
+
652
+ this.onProgress({
653
+ step: 'building',
654
+ progress: progressMap[stage] || 80,
655
+ message: `Building Space... Status: ${stage}`,
656
+ });
657
+ }
658
+
659
+ await new Promise(resolve => setTimeout(resolve, delayMs));
660
+ } catch (err) {
661
+ console.warn('Failed to check space status:', err);
662
+ await new Promise(resolve => setTimeout(resolve, delayMs));
663
+ }
664
+ }
665
+
666
+ // If we get here, the space didn't become ready in time
667
+ console.warn('Space build timeout - it may still be building');
668
+ }
669
+
670
+ /**
671
+ * Main publish method - orchestrates the entire flow
672
+ */
673
+ async publishToSpaces(webcontainer: any, config: PublishConfig): Promise<string> {
674
+ try {
675
+ // Step 1: Collect files
676
+ this.onProgress({
677
+ step: 'collecting',
678
+ progress: 10,
679
+ message: 'Collecting project files from WebContainer...',
680
+ });
681
+
682
+ const zip = await this.collectFilesFromWebContainer(webcontainer);
683
+
684
+ // Step 2: Create ZIP
685
+ this.onProgress({
686
+ step: 'zipping',
687
+ progress: 25,
688
+ message: 'Creating deployment package...',
689
+ });
690
+
691
+ const zipBlob = await zip.generateAsync({
692
+ type: 'blob',
693
+ compression: 'DEFLATE',
694
+ compressionOptions: { level: 6 }
695
+ });
696
+
697
+ // Step 3: Create/Update Space
698
+ this.onProgress({
699
+ step: 'creating',
700
+ progress: 40,
701
+ message: 'Creating Hugging Face Space...',
702
+ });
703
+
704
+ const spaceId = await this.createOrUpdateSpace(config);
705
+
706
+ // Step 4: Upload files
707
+ this.onProgress({
708
+ step: 'uploading',
709
+ progress: 60,
710
+ message: 'Uploading files to Space repository...',
711
+ });
712
+
713
+ await this.uploadToSpace(spaceId, zipBlob, config.hfToken);
714
+
715
+ // Step 5: Wait for build
716
+ this.onProgress({
717
+ step: 'building',
718
+ progress: 80,
719
+ message: 'Building Space environment...',
720
+ });
721
+
722
+ await this.waitForSpaceBuild(spaceId, config.hfToken);
723
+
724
+ // Step 6: Complete
725
+ const spaceUrl = `https://huggingface.co/spaces/${spaceId}`;
726
+
727
+ this.onProgress({
728
+ step: 'complete',
729
+ progress: 100,
730
+ message: `Space published successfully! 🎉`,
731
+ });
732
+
733
+ return spaceUrl;
734
+
735
+ } catch (error) {
736
+ this.onProgress({
737
+ step: 'error',
738
+ progress: 0,
739
+ message: 'Publishing failed',
740
+ error: error instanceof Error ? error.message : 'Unknown error occurred',
741
+ });
742
+ throw error;
743
+ }
744
+ }
745
+ }
746
+
747
+ // Helper function to generate Space-appropriate files
748
+ export const generateSpaceFiles = (webcontainerFiles: Record<string, any>) => {
749
+ const files = { ...webcontainerFiles };
750
+
751
+ // Add README.md if it doesn't exist
752
+ if (!files['README.md']) {
753
+ files['README.md'] = {
754
+ file: {
755
+ contents: `---
756
+ title: My App
757
+ emoji: 🚀
758
+ colorFrom: blue
759
+ colorTo: purple
760
+ sdk: static
761
+ pinned: false
762
+ ---
763
+
764
+ # My App
765
+
766
+ This Space was automatically deployed from WebContainer.
767
+ `
768
+ }
769
+ };
770
+ }
771
+
772
+ // Add .gitignore
773
+ if (!files['.gitignore']) {
774
+ files['.gitignore'] = {
775
+ file: {
776
+ contents: `node_modules/
777
+ .env
778
+ .DS_Store
779
+ *.log
780
+ dist/
781
+ build/
782
+ .next/
783
+ .nuxt/
784
+ `
785
+ }
786
+ };
787
+ }
788
+
789
+ return files;
790
+ };
791
+
792
+ // Usage example:
793
+ /*
794
+ import { HuggingFaceSpacesPublisher, generateSpaceFiles } from './publishUtils';
795
+
796
+ const publisher = new HuggingFaceSpacesPublisher((progress) => {
797
+ console.log(progress);
798
+ // Update your UI here
799
+ });
800
+
801
+ const publishConfig = {
802
+ spaceName: 'my-awesome-app',
803
+ hfToken: 'hf_...',
804
+ spaceType: 'static' as const,
805
+ isPrivate: false,
806
+ description: 'My awesome WebContainer app'
807
+ };
808
+
809
+ const spaceUrl = await publisher.publishToSpaces(webcontainer, publishConfig);
810
+ console.log('Published to:', spaceUrl);
811
+ */
812
+ import classNames from "classnames";
813
+ import { useRef, useEffect, useState, forwardRef } from "react";
814
+ import { TbReload, TbLoader, TbExternalLink, TbUpload, TbCheck, TbX } from "react-icons/tb";
815
+ import { WebContainer } from '@webcontainer/api';
816
+ import { HuggingFaceSpacesPublisher, PublishProgress, PublishConfig } from './publishUtils';
817
+
818
+ // PreviewEye icon component
819
+ const PreviewEye = ({ className = "w-4 h-4" }: { className?: string }) => (
820
+ <svg
821
+ className={className}
822
+ viewBox="0 0 24 24"
823
+ fill="none"
824
+ stroke="currentColor"
825
+ strokeWidth={2}
826
+ strokeLinecap="round"
827
+ strokeLinejoin="round"
828
+ >
829
+ <path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" />
830
+ <circle cx="12" cy="12" r="3" />
831
+ </svg>
832
+ );
833
+
834
+ // Publish Modal Component
835
+ const PublishModal = ({
836
+ isOpen,
837
+ onClose,
838
+ onPublish,
839
+ publishProgress
840
+ }: {
841
+ isOpen: boolean;
842
+ onClose: () => void;
843
+ onPublish: (config: PublishConfig) => void;
844
+ publishProgress: PublishProgress | null;
845
+ }) => {
846
+ const [config, setConfig] = useState<PublishConfig>({
847
+ spaceName: '',
848
+ hfToken: '',
849
+ spaceType: 'static',
850
+ isPrivate: false,
851
+ description: ''
852
+ });
853
+
854
+ const handleSubmit = (e: React.FormEvent) => {
855
+ e.preventDefault();
856
+ onPublish(config);
857
+ };
858
+
859
+ const isPublishing = publishProgress && ['collecting',