mirror of
https://github.com/stackblitz-labs/bolt.diy
synced 2025-06-26 18:26:38 +00:00
feat: fix for push private repo (#1618)
* feat: push private repo # GitHub Integration Changelog ## Fixed - Fixed issue where repositories marked as private weren't being created with private visibility - Added support for changing repository visibility (public/private) when pushing to existing repositories - Fixed 404 errors when pushing files after changing repository visibility ## Added - Added clear user warnings when changing repository visibility from public to private or vice versa - Implemented delays after visibility changes to allow GitHub API to fully process the change - Added retry mechanism (up to 3 attempts with increasing delays) for pushing files after visibility changes - Added repository data refresh before pushing to ensure latest reference data ## Improved - Enhanced error logging and handling for all GitHub API operations - Updated return value handling to use actual repository URLs from the API response - Added comprehensive logging to track repository creation and update operations * cleanup * Update Workbench.client.tsx
This commit is contained in:
parent
552f08acea
commit
0202aefad9
15
.eslintrc.json
Normal file
15
.eslintrc.json
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"env": {
|
||||||
|
"browser": true,
|
||||||
|
"es2021": true
|
||||||
|
},
|
||||||
|
"extends": [
|
||||||
|
"eslint:recommended",
|
||||||
|
"plugin:prettier/recommended"
|
||||||
|
],
|
||||||
|
"rules": {
|
||||||
|
// example: turn off console warnings
|
||||||
|
"no-console": "off"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -136,15 +136,24 @@ export function PushToGitHubDialog({ isOpen, onClose, onPush }: PushToGitHubDial
|
|||||||
const octokit = new Octokit({ auth: connection.token });
|
const octokit = new Octokit({ auth: connection.token });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await octokit.repos.get({
|
const { data: existingRepo } = await octokit.repos.get({
|
||||||
owner: connection.user.login,
|
owner: connection.user.login,
|
||||||
repo: repoName,
|
repo: repoName,
|
||||||
});
|
});
|
||||||
|
|
||||||
// If we get here, the repo exists
|
// If we get here, the repo exists
|
||||||
const confirmOverwrite = window.confirm(
|
let confirmMessage = `Repository "${repoName}" already exists. Do you want to update it? This will add or modify files in the repository.`;
|
||||||
`Repository "${repoName}" already exists. Do you want to update it? This will add or modify files in the repository.`,
|
|
||||||
);
|
// Add visibility change warning if needed
|
||||||
|
if (existingRepo.private !== isPrivate) {
|
||||||
|
const visibilityChange = isPrivate
|
||||||
|
? 'This will also change the repository from public to private.'
|
||||||
|
: 'This will also change the repository from private to public.';
|
||||||
|
|
||||||
|
confirmMessage += `\n\n${visibilityChange}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const confirmOverwrite = window.confirm(confirmMessage);
|
||||||
|
|
||||||
if (!confirmOverwrite) {
|
if (!confirmOverwrite) {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
|
@ -1,6 +1,18 @@
|
|||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { toast as toastify } from 'react-toastify';
|
import { toast as toastify } from 'react-toastify';
|
||||||
|
|
||||||
|
// Configure standard toast settings
|
||||||
|
export const configuredToast = {
|
||||||
|
success: (message: string, options = {}) => toastify.success(message, { autoClose: 3000, ...options }),
|
||||||
|
error: (message: string, options = {}) => toastify.error(message, { autoClose: 3000, ...options }),
|
||||||
|
info: (message: string, options = {}) => toastify.info(message, { autoClose: 3000, ...options }),
|
||||||
|
warning: (message: string, options = {}) => toastify.warning(message, { autoClose: 3000, ...options }),
|
||||||
|
loading: (message: string, options = {}) => toastify.loading(message, { autoClose: 3000, ...options }),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Export the original toast for cases where specific configuration is needed
|
||||||
|
export { toastify as toast };
|
||||||
|
|
||||||
interface ToastOptions {
|
interface ToastOptions {
|
||||||
type?: 'success' | 'error' | 'info' | 'warning';
|
type?: 'success' | 'error' | 'info' | 'warning';
|
||||||
duration?: number;
|
duration?: number;
|
||||||
@ -36,5 +48,19 @@ export function useToast() {
|
|||||||
[toast],
|
[toast],
|
||||||
);
|
);
|
||||||
|
|
||||||
return { toast, success, error };
|
const info = useCallback(
|
||||||
|
(message: string, options: Omit<ToastOptions, 'type'> = {}) => {
|
||||||
|
toast(message, { ...options, type: 'info' });
|
||||||
|
},
|
||||||
|
[toast],
|
||||||
|
);
|
||||||
|
|
||||||
|
const warning = useCallback(
|
||||||
|
(message: string, options: Omit<ToastOptions, 'type'> = {}) => {
|
||||||
|
toast(message, { ...options, type: 'warning' });
|
||||||
|
},
|
||||||
|
[toast],
|
||||||
|
);
|
||||||
|
|
||||||
|
return { toast, success, error, info, warning };
|
||||||
}
|
}
|
||||||
|
@ -388,11 +388,9 @@ export const Workbench = memo(
|
|||||||
Toggle Terminal
|
Toggle Terminal
|
||||||
</PanelHeaderButton>
|
</PanelHeaderButton>
|
||||||
<DropdownMenu.Root>
|
<DropdownMenu.Root>
|
||||||
<DropdownMenu.Trigger className="bg-transparent">
|
<DropdownMenu.Trigger className="text-sm flex items-center gap-1 text-bolt-elements-item-contentDefault bg-transparent enabled:hover:text-bolt-elements-item-contentActive rounded-md p-1 enabled:hover:bg-bolt-elements-item-backgroundActive disabled:cursor-not-allowed">
|
||||||
<button className="text-sm flex items-center gap-1 text-bolt-elements-item-contentDefault bg-transparent enabled:hover:text-bolt-elements-item-contentActive rounded-md p-1 enabled:hover:bg-bolt-elements-item-backgroundActive disabled:cursor-not-allowed">
|
<div className="i-ph:box-arrow-up" />
|
||||||
<div className="i-ph:box-arrow-up" />
|
Sync & Export
|
||||||
Sync & Export
|
|
||||||
</button>
|
|
||||||
</DropdownMenu.Trigger>
|
</DropdownMenu.Trigger>
|
||||||
<DropdownMenu.Content
|
<DropdownMenu.Content
|
||||||
className={classNames(
|
className={classNames(
|
||||||
@ -491,12 +489,12 @@ export const Workbench = memo(
|
|||||||
<PushToGitHubDialog
|
<PushToGitHubDialog
|
||||||
isOpen={isPushDialogOpen}
|
isOpen={isPushDialogOpen}
|
||||||
onClose={() => setIsPushDialogOpen(false)}
|
onClose={() => setIsPushDialogOpen(false)}
|
||||||
onPush={async (repoName, username, token) => {
|
onPush={async (repoName, username, token, isPrivate) => {
|
||||||
try {
|
try {
|
||||||
const commitMessage = prompt('Please enter a commit message:', 'Initial commit') || 'Initial commit';
|
console.log('Dialog onPush called with isPrivate =', isPrivate);
|
||||||
await workbenchStore.pushToGitHub(repoName, commitMessage, username, token);
|
|
||||||
|
|
||||||
const repoUrl = `https://github.com/${username}/${repoName}`;
|
const commitMessage = prompt('Please enter a commit message:', 'Initial commit') || 'Initial commit';
|
||||||
|
const repoUrl = await workbenchStore.pushToGitHub(repoName, commitMessage, username, token, isPrivate);
|
||||||
|
|
||||||
if (updateChatMestaData && !metadata?.gitUrl) {
|
if (updateChatMestaData && !metadata?.gitUrl) {
|
||||||
updateChatMestaData({
|
updateChatMestaData({
|
||||||
|
@ -600,7 +600,13 @@ export class WorkbenchStore {
|
|||||||
return syncedFiles;
|
return syncedFiles;
|
||||||
}
|
}
|
||||||
|
|
||||||
async pushToGitHub(repoName: string, commitMessage?: string, githubUsername?: string, ghToken?: string) {
|
async pushToGitHub(
|
||||||
|
repoName: string,
|
||||||
|
commitMessage?: string,
|
||||||
|
githubUsername?: string,
|
||||||
|
ghToken?: string,
|
||||||
|
isPrivate: boolean = false,
|
||||||
|
) {
|
||||||
try {
|
try {
|
||||||
// Use cookies if username and token are not provided
|
// Use cookies if username and token are not provided
|
||||||
const githubToken = ghToken || Cookies.get('githubToken');
|
const githubToken = ghToken || Cookies.get('githubToken');
|
||||||
@ -610,26 +616,72 @@ export class WorkbenchStore {
|
|||||||
throw new Error('GitHub token or username is not set in cookies or provided.');
|
throw new Error('GitHub token or username is not set in cookies or provided.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Log the isPrivate flag to verify it's being properly passed
|
||||||
|
console.log(`pushToGitHub called with isPrivate=${isPrivate}`);
|
||||||
|
|
||||||
// Initialize Octokit with the auth token
|
// Initialize Octokit with the auth token
|
||||||
const octokit = new Octokit({ auth: githubToken });
|
const octokit = new Octokit({ auth: githubToken });
|
||||||
|
|
||||||
// Check if the repository already exists before creating it
|
// Check if the repository already exists before creating it
|
||||||
let repo: RestEndpointMethodTypes['repos']['get']['response']['data'];
|
let repo: RestEndpointMethodTypes['repos']['get']['response']['data'];
|
||||||
|
let visibilityJustChanged = false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const resp = await octokit.repos.get({ owner, repo: repoName });
|
const resp = await octokit.repos.get({ owner, repo: repoName });
|
||||||
repo = resp.data;
|
repo = resp.data;
|
||||||
|
console.log('Repository already exists, using existing repo');
|
||||||
|
|
||||||
|
// Check if we need to update visibility of existing repo
|
||||||
|
if (repo.private !== isPrivate) {
|
||||||
|
console.log(
|
||||||
|
`Updating repository visibility from ${repo.private ? 'private' : 'public'} to ${isPrivate ? 'private' : 'public'}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Update repository visibility using the update method
|
||||||
|
const { data: updatedRepo } = await octokit.repos.update({
|
||||||
|
owner,
|
||||||
|
repo: repoName,
|
||||||
|
private: isPrivate,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Repository visibility updated successfully');
|
||||||
|
repo = updatedRepo;
|
||||||
|
visibilityJustChanged = true;
|
||||||
|
|
||||||
|
// Add a delay after changing visibility to allow GitHub to fully process the change
|
||||||
|
console.log('Waiting for visibility change to propagate...');
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 3000)); // 3 second delay
|
||||||
|
} catch (visibilityError) {
|
||||||
|
console.error('Failed to update repository visibility:', visibilityError);
|
||||||
|
|
||||||
|
// Continue with push even if visibility update fails
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof Error && 'status' in error && error.status === 404) {
|
if (error instanceof Error && 'status' in error && error.status === 404) {
|
||||||
// Repository doesn't exist, so create a new one
|
// Repository doesn't exist, so create a new one
|
||||||
const { data: newRepo } = await octokit.repos.createForAuthenticatedUser({
|
console.log(`Creating new repository with private=${isPrivate}`);
|
||||||
|
|
||||||
|
// Create new repository with specified privacy setting
|
||||||
|
const createRepoOptions = {
|
||||||
name: repoName,
|
name: repoName,
|
||||||
private: false,
|
private: isPrivate,
|
||||||
auto_init: true,
|
auto_init: true,
|
||||||
});
|
};
|
||||||
|
|
||||||
|
console.log('Create repo options:', createRepoOptions);
|
||||||
|
|
||||||
|
const { data: newRepo } = await octokit.repos.createForAuthenticatedUser(createRepoOptions);
|
||||||
|
|
||||||
|
console.log('Repository created:', newRepo.html_url, 'Private:', newRepo.private);
|
||||||
repo = newRepo;
|
repo = newRepo;
|
||||||
|
|
||||||
|
// Add a small delay after creating a repository to allow GitHub to fully initialize it
|
||||||
|
console.log('Waiting for repository to initialize...');
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 2000)); // 2 second delay
|
||||||
} else {
|
} else {
|
||||||
console.log('cannot create repo!');
|
console.error('Cannot create repo:', error);
|
||||||
throw error; // Some other error occurred
|
throw error; // Some other error occurred
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -641,68 +693,102 @@ export class WorkbenchStore {
|
|||||||
throw new Error('No files found to push');
|
throw new Error('No files found to push');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create blobs for each file
|
// Function to push files with retry logic
|
||||||
const blobs = await Promise.all(
|
const pushFilesToRepo = async (attempt = 1): Promise<string> => {
|
||||||
Object.entries(files).map(async ([filePath, dirent]) => {
|
const maxAttempts = 3;
|
||||||
if (dirent?.type === 'file' && dirent.content) {
|
|
||||||
const { data: blob } = await octokit.git.createBlob({
|
try {
|
||||||
owner: repo.owner.login,
|
console.log(`Pushing files to repository (attempt ${attempt}/${maxAttempts})...`);
|
||||||
repo: repo.name,
|
|
||||||
content: Buffer.from(dirent.content).toString('base64'),
|
// Create blobs for each file
|
||||||
encoding: 'base64',
|
const blobs = await Promise.all(
|
||||||
});
|
Object.entries(files).map(async ([filePath, dirent]) => {
|
||||||
return { path: extractRelativePath(filePath), sha: blob.sha };
|
if (dirent?.type === 'file' && dirent.content) {
|
||||||
|
const { data: blob } = await octokit.git.createBlob({
|
||||||
|
owner: repo.owner.login,
|
||||||
|
repo: repo.name,
|
||||||
|
content: Buffer.from(dirent.content).toString('base64'),
|
||||||
|
encoding: 'base64',
|
||||||
|
});
|
||||||
|
return { path: extractRelativePath(filePath), sha: blob.sha };
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const validBlobs = blobs.filter(Boolean); // Filter out any undefined blobs
|
||||||
|
|
||||||
|
if (validBlobs.length === 0) {
|
||||||
|
throw new Error('No valid files to push');
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
// Refresh repository reference to ensure we have the latest data
|
||||||
}),
|
const repoRefresh = await octokit.repos.get({ owner, repo: repoName });
|
||||||
);
|
repo = repoRefresh.data;
|
||||||
|
|
||||||
const validBlobs = blobs.filter(Boolean); // Filter out any undefined blobs
|
// Get the latest commit SHA (assuming main branch, update dynamically if needed)
|
||||||
|
const { data: ref } = await octokit.git.getRef({
|
||||||
|
owner: repo.owner.login,
|
||||||
|
repo: repo.name,
|
||||||
|
ref: `heads/${repo.default_branch || 'main'}`, // Handle dynamic branch
|
||||||
|
});
|
||||||
|
const latestCommitSha = ref.object.sha;
|
||||||
|
|
||||||
if (validBlobs.length === 0) {
|
// Create a new tree
|
||||||
throw new Error('No valid files to push');
|
const { data: newTree } = await octokit.git.createTree({
|
||||||
}
|
owner: repo.owner.login,
|
||||||
|
repo: repo.name,
|
||||||
|
base_tree: latestCommitSha,
|
||||||
|
tree: validBlobs.map((blob) => ({
|
||||||
|
path: blob!.path,
|
||||||
|
mode: '100644',
|
||||||
|
type: 'blob',
|
||||||
|
sha: blob!.sha,
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
|
||||||
// Get the latest commit SHA (assuming main branch, update dynamically if needed)
|
// Create a new commit
|
||||||
const { data: ref } = await octokit.git.getRef({
|
const { data: newCommit } = await octokit.git.createCommit({
|
||||||
owner: repo.owner.login,
|
owner: repo.owner.login,
|
||||||
repo: repo.name,
|
repo: repo.name,
|
||||||
ref: `heads/${repo.default_branch || 'main'}`, // Handle dynamic branch
|
message: commitMessage || 'Initial commit from your app',
|
||||||
});
|
tree: newTree.sha,
|
||||||
const latestCommitSha = ref.object.sha;
|
parents: [latestCommitSha],
|
||||||
|
});
|
||||||
|
|
||||||
// Create a new tree
|
// Update the reference
|
||||||
const { data: newTree } = await octokit.git.createTree({
|
await octokit.git.updateRef({
|
||||||
owner: repo.owner.login,
|
owner: repo.owner.login,
|
||||||
repo: repo.name,
|
repo: repo.name,
|
||||||
base_tree: latestCommitSha,
|
ref: `heads/${repo.default_branch || 'main'}`, // Handle dynamic branch
|
||||||
tree: validBlobs.map((blob) => ({
|
sha: newCommit.sha,
|
||||||
path: blob!.path,
|
});
|
||||||
mode: '100644',
|
|
||||||
type: 'blob',
|
|
||||||
sha: blob!.sha,
|
|
||||||
})),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create a new commit
|
console.log('Files successfully pushed to repository');
|
||||||
const { data: newCommit } = await octokit.git.createCommit({
|
|
||||||
owner: repo.owner.login,
|
|
||||||
repo: repo.name,
|
|
||||||
message: commitMessage || 'Initial commit from your app',
|
|
||||||
tree: newTree.sha,
|
|
||||||
parents: [latestCommitSha],
|
|
||||||
});
|
|
||||||
|
|
||||||
// Update the reference
|
return repo.html_url;
|
||||||
await octokit.git.updateRef({
|
} catch (error) {
|
||||||
owner: repo.owner.login,
|
console.error(`Error during push attempt ${attempt}:`, error);
|
||||||
repo: repo.name,
|
|
||||||
ref: `heads/${repo.default_branch || 'main'}`, // Handle dynamic branch
|
|
||||||
sha: newCommit.sha,
|
|
||||||
});
|
|
||||||
|
|
||||||
alert(`Repository created and code pushed: ${repo.html_url}`);
|
// If we've just changed visibility and this is not our last attempt, wait and retry
|
||||||
|
if ((visibilityJustChanged || attempt === 1) && attempt < maxAttempts) {
|
||||||
|
const delayMs = attempt * 2000; // Increasing delay with each attempt
|
||||||
|
console.log(`Waiting ${delayMs}ms before retry...`);
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
||||||
|
|
||||||
|
return pushFilesToRepo(attempt + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error; // Rethrow if we're out of attempts
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Execute the push function with retry logic
|
||||||
|
const repoUrl = await pushFilesToRepo();
|
||||||
|
|
||||||
|
// Return the repository URL
|
||||||
|
return repoUrl;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error pushing to GitHub:', error);
|
console.error('Error pushing to GitHub:', error);
|
||||||
throw error; // Rethrow the error for further handling
|
throw error; // Rethrow the error for further handling
|
||||||
|
229
app/utils/file-watcher.ts
Normal file
229
app/utils/file-watcher.ts
Normal file
@ -0,0 +1,229 @@
|
|||||||
|
import type { WebContainer } from '@webcontainer/api';
|
||||||
|
import { WORK_DIR } from './constants';
|
||||||
|
|
||||||
|
// Global object to track watcher state
|
||||||
|
const watcherState = {
|
||||||
|
fallbackEnabled: tryLoadFallbackState(),
|
||||||
|
watchingPaths: new Set<string>(),
|
||||||
|
callbacks: new Map<string, Set<() => void>>(),
|
||||||
|
pollingInterval: null as NodeJS.Timeout | null,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Try to load the fallback state from localStorage
|
||||||
|
function tryLoadFallbackState(): boolean {
|
||||||
|
try {
|
||||||
|
if (typeof localStorage !== 'undefined') {
|
||||||
|
const state = localStorage.getItem('bolt-file-watcher-fallback');
|
||||||
|
return state === 'true';
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
console.warn('[FileWatcher] Failed to load fallback state from localStorage');
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save the fallback state to localStorage
|
||||||
|
function saveFallbackState(state: boolean) {
|
||||||
|
try {
|
||||||
|
if (typeof localStorage !== 'undefined') {
|
||||||
|
localStorage.setItem('bolt-file-watcher-fallback', state ? 'true' : 'false');
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
console.warn('[FileWatcher] Failed to save fallback state to localStorage');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Safe file watcher that falls back to polling when native file watching fails
|
||||||
|
*
|
||||||
|
* @param webcontainer The WebContainer instance
|
||||||
|
* @param pattern File pattern to watch
|
||||||
|
* @param callback Function to call when files change
|
||||||
|
* @returns An object with a close method
|
||||||
|
*/
|
||||||
|
export async function safeWatch(webcontainer: WebContainer, pattern: string = '**/*', callback: () => void) {
|
||||||
|
// Register the callback
|
||||||
|
if (!watcherState.callbacks.has(pattern)) {
|
||||||
|
watcherState.callbacks.set(pattern, new Set());
|
||||||
|
}
|
||||||
|
|
||||||
|
watcherState.callbacks.get(pattern)!.add(callback);
|
||||||
|
|
||||||
|
// If we're already using fallback mode, don't try native watchers again
|
||||||
|
if (watcherState.fallbackEnabled) {
|
||||||
|
// Make sure polling is active
|
||||||
|
ensurePollingActive();
|
||||||
|
|
||||||
|
// Return a cleanup function
|
||||||
|
return {
|
||||||
|
close: () => {
|
||||||
|
const callbacks = watcherState.callbacks.get(pattern);
|
||||||
|
|
||||||
|
if (callbacks) {
|
||||||
|
callbacks.delete(callback);
|
||||||
|
|
||||||
|
if (callbacks.size === 0) {
|
||||||
|
watcherState.callbacks.delete(pattern);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to use native file watching
|
||||||
|
try {
|
||||||
|
const watcher = await webcontainer.fs.watch(pattern, { persistent: true });
|
||||||
|
watcherState.watchingPaths.add(pattern);
|
||||||
|
|
||||||
|
// Use the native watch events
|
||||||
|
(watcher as any).addEventListener('change', () => {
|
||||||
|
// Call all callbacks for this pattern
|
||||||
|
const callbacks = watcherState.callbacks.get(pattern);
|
||||||
|
|
||||||
|
if (callbacks) {
|
||||||
|
callbacks.forEach((cb) => cb());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Return an object with a close method
|
||||||
|
return {
|
||||||
|
close: () => {
|
||||||
|
try {
|
||||||
|
watcher.close();
|
||||||
|
watcherState.watchingPaths.delete(pattern);
|
||||||
|
|
||||||
|
const callbacks = watcherState.callbacks.get(pattern);
|
||||||
|
|
||||||
|
if (callbacks) {
|
||||||
|
callbacks.delete(callback);
|
||||||
|
|
||||||
|
if (callbacks.size === 0) {
|
||||||
|
watcherState.callbacks.delete(pattern);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('[FileWatcher] Error closing watcher:', error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('[FileWatcher] Native file watching failed:', error);
|
||||||
|
console.info('[FileWatcher] Falling back to polling mechanism for file changes');
|
||||||
|
|
||||||
|
// Switch to fallback mode for all future watches
|
||||||
|
watcherState.fallbackEnabled = true;
|
||||||
|
saveFallbackState(true);
|
||||||
|
|
||||||
|
// Start polling
|
||||||
|
ensurePollingActive();
|
||||||
|
|
||||||
|
// Return a mock watcher object
|
||||||
|
return {
|
||||||
|
close: () => {
|
||||||
|
const callbacks = watcherState.callbacks.get(pattern);
|
||||||
|
|
||||||
|
if (callbacks) {
|
||||||
|
callbacks.delete(callback);
|
||||||
|
|
||||||
|
if (callbacks.size === 0) {
|
||||||
|
watcherState.callbacks.delete(pattern);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no more callbacks, stop polling
|
||||||
|
if (watcherState.callbacks.size === 0 && watcherState.pollingInterval) {
|
||||||
|
clearInterval(watcherState.pollingInterval);
|
||||||
|
watcherState.pollingInterval = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure polling is active
|
||||||
|
function ensurePollingActive() {
|
||||||
|
if (watcherState.pollingInterval) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up a polling interval that calls all callbacks
|
||||||
|
watcherState.pollingInterval = setInterval(() => {
|
||||||
|
// Call all registered callbacks
|
||||||
|
for (const [, callbacks] of watcherState.callbacks.entries()) {
|
||||||
|
callbacks.forEach((callback) => callback());
|
||||||
|
}
|
||||||
|
}, 3000); // Poll every 3 seconds
|
||||||
|
|
||||||
|
// Clean up interval when window unloads
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
window.addEventListener('beforeunload', () => {
|
||||||
|
if (watcherState.pollingInterval) {
|
||||||
|
clearInterval(watcherState.pollingInterval);
|
||||||
|
watcherState.pollingInterval = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SafeWatchPaths mimics the webcontainer.internal.watchPaths method but with fallback
|
||||||
|
export function safeWatchPaths(
|
||||||
|
webcontainer: WebContainer,
|
||||||
|
config: { include: string[]; exclude?: string[]; includeContent?: boolean },
|
||||||
|
callback: any,
|
||||||
|
) {
|
||||||
|
// Create a valid mock event to prevent undefined errors
|
||||||
|
const createMockEvent = () => ({
|
||||||
|
type: 'change',
|
||||||
|
path: `${WORK_DIR}/mock-path.txt`,
|
||||||
|
buffer: new Uint8Array(0),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Start with polling if we already know native watching doesn't work
|
||||||
|
if (watcherState.fallbackEnabled) {
|
||||||
|
console.info('[FileWatcher] Using fallback polling for watchPaths');
|
||||||
|
ensurePollingActive();
|
||||||
|
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
// Use our helper to create a valid event
|
||||||
|
const mockEvent = createMockEvent();
|
||||||
|
|
||||||
|
// Wrap in the expected structure of nested arrays
|
||||||
|
callback([[mockEvent]]);
|
||||||
|
}, 3000);
|
||||||
|
|
||||||
|
return {
|
||||||
|
close: () => {
|
||||||
|
clearInterval(interval);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try native watching
|
||||||
|
try {
|
||||||
|
return webcontainer.internal.watchPaths(config, callback);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('[FileWatcher] Native watchPaths failed:', error);
|
||||||
|
console.info('[FileWatcher] Using fallback polling for watchPaths');
|
||||||
|
|
||||||
|
// Mark as using fallback
|
||||||
|
watcherState.fallbackEnabled = true;
|
||||||
|
saveFallbackState(true);
|
||||||
|
|
||||||
|
// Set up polling
|
||||||
|
ensurePollingActive();
|
||||||
|
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
// Use our helper to create a valid event
|
||||||
|
const mockEvent = createMockEvent();
|
||||||
|
|
||||||
|
// Wrap in the expected structure of nested arrays
|
||||||
|
callback([[mockEvent]]);
|
||||||
|
}, 3000);
|
||||||
|
|
||||||
|
return {
|
||||||
|
close: () => {
|
||||||
|
clearInterval(interval);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
@ -181,13 +181,15 @@
|
|||||||
"crypto-browserify": "^3.12.1",
|
"crypto-browserify": "^3.12.1",
|
||||||
"electron": "^33.2.0",
|
"electron": "^33.2.0",
|
||||||
"electron-builder": "^25.1.8",
|
"electron-builder": "^25.1.8",
|
||||||
|
"eslint-config-prettier": "^10.1.1",
|
||||||
|
"eslint-plugin-prettier": "^5.2.6",
|
||||||
"fast-glob": "^3.3.2",
|
"fast-glob": "^3.3.2",
|
||||||
"husky": "9.1.7",
|
"husky": "9.1.7",
|
||||||
"is-ci": "^3.0.1",
|
"is-ci": "^3.0.1",
|
||||||
"jsdom": "^26.0.0",
|
"jsdom": "^26.0.0",
|
||||||
"node-fetch": "^3.3.2",
|
"node-fetch": "^3.3.2",
|
||||||
"pnpm": "^9.14.4",
|
"pnpm": "^9.14.4",
|
||||||
"prettier": "^3.4.1",
|
"prettier": "^3.5.3",
|
||||||
"rimraf": "^4.4.1",
|
"rimraf": "^4.4.1",
|
||||||
"sass-embedded": "^1.81.0",
|
"sass-embedded": "^1.81.0",
|
||||||
"stream-browserify": "^3.0.0",
|
"stream-browserify": "^3.0.0",
|
||||||
|
@ -405,6 +405,12 @@ importers:
|
|||||||
electron-builder:
|
electron-builder:
|
||||||
specifier: ^25.1.8
|
specifier: ^25.1.8
|
||||||
version: 25.1.8(electron-builder-squirrel-windows@25.1.8(dmg-builder@25.1.8))
|
version: 25.1.8(electron-builder-squirrel-windows@25.1.8(dmg-builder@25.1.8))
|
||||||
|
eslint-config-prettier:
|
||||||
|
specifier: ^10.1.1
|
||||||
|
version: 10.1.1(eslint@9.23.0(jiti@1.21.7))
|
||||||
|
eslint-plugin-prettier:
|
||||||
|
specifier: ^5.2.6
|
||||||
|
version: 5.2.6(eslint-config-prettier@10.1.1(eslint@9.23.0(jiti@1.21.7)))(eslint@9.23.0(jiti@1.21.7))(prettier@3.5.3)
|
||||||
fast-glob:
|
fast-glob:
|
||||||
specifier: ^3.3.2
|
specifier: ^3.3.2
|
||||||
version: 3.3.3
|
version: 3.3.3
|
||||||
@ -424,7 +430,7 @@ importers:
|
|||||||
specifier: ^9.14.4
|
specifier: ^9.14.4
|
||||||
version: 9.15.9
|
version: 9.15.9
|
||||||
prettier:
|
prettier:
|
||||||
specifier: ^3.4.1
|
specifier: ^3.5.3
|
||||||
version: 3.5.3
|
version: 3.5.3
|
||||||
rimraf:
|
rimraf:
|
||||||
specifier: ^4.4.1
|
specifier: ^4.4.1
|
||||||
@ -2186,6 +2192,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-vsJDAkYR6qCPu+ioGScGiMYR7LvZYIXh/dlQeviqoTWNCVfKTLYD/LkNWH4Mxsv2a5vpIRc77FN5DnmK1eBggQ==}
|
resolution: {integrity: sha512-vsJDAkYR6qCPu+ioGScGiMYR7LvZYIXh/dlQeviqoTWNCVfKTLYD/LkNWH4Mxsv2a5vpIRc77FN5DnmK1eBggQ==}
|
||||||
engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
|
engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
|
||||||
|
|
||||||
|
'@pkgr/core@0.2.1':
|
||||||
|
resolution: {integrity: sha512-VzgHzGblFmUeBmmrk55zPyrQIArQN4vujc9shWytaPdB3P7qhi0cpaiKIr7tlCmFv2lYUwnLospIqjL9ZSAhhg==}
|
||||||
|
engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
|
||||||
|
|
||||||
'@polka/url@1.0.0-next.28':
|
'@polka/url@1.0.0-next.28':
|
||||||
resolution: {integrity: sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw==}
|
resolution: {integrity: sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw==}
|
||||||
|
|
||||||
@ -4554,6 +4564,12 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
eslint: '>=6.0.0'
|
eslint: '>=6.0.0'
|
||||||
|
|
||||||
|
eslint-config-prettier@10.1.1:
|
||||||
|
resolution: {integrity: sha512-4EQQr6wXwS+ZJSzaR5ZCrYgLxqvUjdXctaEtBqHcbkW944B1NQyO4qpdHQbXBONfwxXdkAY81HH4+LUfrg+zPw==}
|
||||||
|
hasBin: true
|
||||||
|
peerDependencies:
|
||||||
|
eslint: '>=7.0.0'
|
||||||
|
|
||||||
eslint-config-prettier@9.1.0:
|
eslint-config-prettier@9.1.0:
|
||||||
resolution: {integrity: sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==}
|
resolution: {integrity: sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
@ -4577,8 +4593,8 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
eslint: '>=6.0.0'
|
eslint: '>=6.0.0'
|
||||||
|
|
||||||
eslint-plugin-prettier@5.2.5:
|
eslint-plugin-prettier@5.2.6:
|
||||||
resolution: {integrity: sha512-IKKP8R87pJyMl7WWamLgPkloB16dagPIdd2FjBDbyRYPKo93wS/NbCOPh6gH+ieNLC+XZrhJt/kWj0PS/DFdmg==}
|
resolution: {integrity: sha512-mUcf7QG2Tjk7H055Jk0lGBjbgDnfrvqjhXh9t2xLMSCjZVcw9Rb1V6sVNXO0th3jgeO7zllWPTNRil3JW94TnQ==}
|
||||||
engines: {node: ^14.18.0 || >=16.0.0}
|
engines: {node: ^14.18.0 || >=16.0.0}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@types/eslint': '>=8.0.0'
|
'@types/eslint': '>=8.0.0'
|
||||||
@ -7389,6 +7405,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-R1urvuyiTaWfeCggqEvpDJwAlDVdsT9NM+IP//Tk2x7qHCkSvBk/fwFgw/TLAHzZlrAnnazMcRw0ZD8HlYFTEQ==}
|
resolution: {integrity: sha512-R1urvuyiTaWfeCggqEvpDJwAlDVdsT9NM+IP//Tk2x7qHCkSvBk/fwFgw/TLAHzZlrAnnazMcRw0ZD8HlYFTEQ==}
|
||||||
engines: {node: ^14.18.0 || >=16.0.0}
|
engines: {node: ^14.18.0 || >=16.0.0}
|
||||||
|
|
||||||
|
synckit@0.11.3:
|
||||||
|
resolution: {integrity: sha512-szhWDqNNI9etJUvbZ1/cx1StnZx8yMmFxme48SwR4dty4ioSY50KEZlpv0qAfgc1fpRzuh9hBXEzoCpJ779dLg==}
|
||||||
|
engines: {node: ^14.18.0 || >=16.0.0}
|
||||||
|
|
||||||
tabbable@6.2.0:
|
tabbable@6.2.0:
|
||||||
resolution: {integrity: sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==}
|
resolution: {integrity: sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==}
|
||||||
|
|
||||||
@ -8837,7 +8857,7 @@ snapshots:
|
|||||||
eslint: 9.23.0(jiti@1.21.7)
|
eslint: 9.23.0(jiti@1.21.7)
|
||||||
eslint-config-prettier: 9.1.0(eslint@9.23.0(jiti@1.21.7))
|
eslint-config-prettier: 9.1.0(eslint@9.23.0(jiti@1.21.7))
|
||||||
eslint-plugin-jsonc: 2.20.0(eslint@9.23.0(jiti@1.21.7))
|
eslint-plugin-jsonc: 2.20.0(eslint@9.23.0(jiti@1.21.7))
|
||||||
eslint-plugin-prettier: 5.2.5(eslint-config-prettier@9.1.0(eslint@9.23.0(jiti@1.21.7)))(eslint@9.23.0(jiti@1.21.7))(prettier@3.5.3)
|
eslint-plugin-prettier: 5.2.6(eslint-config-prettier@9.1.0(eslint@9.23.0(jiti@1.21.7)))(eslint@9.23.0(jiti@1.21.7))(prettier@3.5.3)
|
||||||
globals: 15.15.0
|
globals: 15.15.0
|
||||||
typescript-eslint: 8.28.0(eslint@9.23.0(jiti@1.21.7))(typescript@5.8.2)
|
typescript-eslint: 8.28.0(eslint@9.23.0(jiti@1.21.7))(typescript@5.8.2)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
@ -9938,6 +9958,8 @@ snapshots:
|
|||||||
|
|
||||||
'@pkgr/core@0.2.0': {}
|
'@pkgr/core@0.2.0': {}
|
||||||
|
|
||||||
|
'@pkgr/core@0.2.1': {}
|
||||||
|
|
||||||
'@polka/url@1.0.0-next.28': {}
|
'@polka/url@1.0.0-next.28': {}
|
||||||
|
|
||||||
'@radix-ui/number@1.1.0': {}
|
'@radix-ui/number@1.1.0': {}
|
||||||
@ -12925,6 +12947,10 @@ snapshots:
|
|||||||
eslint: 9.23.0(jiti@1.21.7)
|
eslint: 9.23.0(jiti@1.21.7)
|
||||||
semver: 7.7.1
|
semver: 7.7.1
|
||||||
|
|
||||||
|
eslint-config-prettier@10.1.1(eslint@9.23.0(jiti@1.21.7)):
|
||||||
|
dependencies:
|
||||||
|
eslint: 9.23.0(jiti@1.21.7)
|
||||||
|
|
||||||
eslint-config-prettier@9.1.0(eslint@9.23.0(jiti@1.21.7)):
|
eslint-config-prettier@9.1.0(eslint@9.23.0(jiti@1.21.7)):
|
||||||
dependencies:
|
dependencies:
|
||||||
eslint: 9.23.0(jiti@1.21.7)
|
eslint: 9.23.0(jiti@1.21.7)
|
||||||
@ -12949,12 +12975,21 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@eslint/json'
|
- '@eslint/json'
|
||||||
|
|
||||||
eslint-plugin-prettier@5.2.5(eslint-config-prettier@9.1.0(eslint@9.23.0(jiti@1.21.7)))(eslint@9.23.0(jiti@1.21.7))(prettier@3.5.3):
|
eslint-plugin-prettier@5.2.6(eslint-config-prettier@10.1.1(eslint@9.23.0(jiti@1.21.7)))(eslint@9.23.0(jiti@1.21.7))(prettier@3.5.3):
|
||||||
dependencies:
|
dependencies:
|
||||||
eslint: 9.23.0(jiti@1.21.7)
|
eslint: 9.23.0(jiti@1.21.7)
|
||||||
prettier: 3.5.3
|
prettier: 3.5.3
|
||||||
prettier-linter-helpers: 1.0.0
|
prettier-linter-helpers: 1.0.0
|
||||||
synckit: 0.10.3
|
synckit: 0.11.3
|
||||||
|
optionalDependencies:
|
||||||
|
eslint-config-prettier: 10.1.1(eslint@9.23.0(jiti@1.21.7))
|
||||||
|
|
||||||
|
eslint-plugin-prettier@5.2.6(eslint-config-prettier@9.1.0(eslint@9.23.0(jiti@1.21.7)))(eslint@9.23.0(jiti@1.21.7))(prettier@3.5.3):
|
||||||
|
dependencies:
|
||||||
|
eslint: 9.23.0(jiti@1.21.7)
|
||||||
|
prettier: 3.5.3
|
||||||
|
prettier-linter-helpers: 1.0.0
|
||||||
|
synckit: 0.11.3
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
eslint-config-prettier: 9.1.0(eslint@9.23.0(jiti@1.21.7))
|
eslint-config-prettier: 9.1.0(eslint@9.23.0(jiti@1.21.7))
|
||||||
|
|
||||||
@ -16448,6 +16483,11 @@ snapshots:
|
|||||||
'@pkgr/core': 0.2.0
|
'@pkgr/core': 0.2.0
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
|
|
||||||
|
synckit@0.11.3:
|
||||||
|
dependencies:
|
||||||
|
'@pkgr/core': 0.2.1
|
||||||
|
tslib: 2.8.1
|
||||||
|
|
||||||
tabbable@6.2.0: {}
|
tabbable@6.2.0: {}
|
||||||
|
|
||||||
tailwind-merge@2.6.0: {}
|
tailwind-merge@2.6.0: {}
|
||||||
|
Loading…
Reference in New Issue
Block a user