Skip to content
项目
群组
代码片段
帮助
当前项目
正在载入...
登录 / 注册
切换导航面板
B
bit-pm
项目
项目
详情
活动
周期分析
仓库
仓库
文件
提交
分支
标签
贡献者
图表
比较
统计图
议题
0
议题
0
列表
看板
标记
里程碑
合并请求
0
合并请求
0
CI / CD
CI / CD
流水线
作业
日程
统计图
Wiki
Wiki
代码片段
代码片段
成员
成员
折叠边栏
关闭边栏
活动
图像
聊天
创建新问题
作业
提交
问题看板
Open sidebar
燕伟桐
bit-pm
Commits
dfdd267f
Unverified
提交
dfdd267f
authored
7月 08, 2025
作者:
Will Chen
提交者:
GitHub
7月 08, 2025
浏览文件
操作
浏览文件
下载
电子邮件补丁
差异文件
Backup Dyad on new versions (#595)
上级
b6fd985d
全部展开
隐藏空白字符变更
内嵌
并排
正在显示
11 个修改的文件
包含
305 行增加
和
35 行删除
+305
-35
backup.spec.ts
e2e-tests/backup.spec.ts
+230
-0
empty-v0.12.0-beta.1.db
e2e-tests/fixtures/backups/empty-v0.12.0-beta.1.db
+0
-0
test_helper.ts
e2e-tests/helpers/test_helper.ts
+28
-8
problems.spec.ts_problems---manual-edit-next-js-1.aria.yml
...roblems.spec.ts_problems---manual-edit-next-js-1.aria.yml
+1
-1
problems.spec.ts_problems---manual-edit-next-js-2.aria.yml
...roblems.spec.ts_problems---manual-edit-next-js-2.aria.yml
+3
-1
problems.spec.ts_problems---manual-edit-react-vite-1.aria.yml
...lems.spec.ts_problems---manual-edit-react-vite-1.aria.yml
+1
-1
problems.spec.ts_problems---manual-edit-react-vite-2.aria.yml
...lems.spec.ts_problems---manual-edit-react-vite-2.aria.yml
+3
-1
backup_manager.ts
src/backup_manager.ts
+0
-0
index.ts
src/db/index.ts
+19
-14
main.ts
src/main.ts
+19
-8
settings.ts
src/main/settings.ts
+1
-1
没有找到文件。
e2e-tests/backup.spec.ts
0 → 100644
浏览文件 @
dfdd267f
import
*
as
path
from
"path"
;
import
*
as
fs
from
"fs"
;
import
*
as
crypto
from
"crypto"
;
import
{
testWithConfig
,
test
,
PageObject
}
from
"./helpers/test_helper"
;
import
{
expect
}
from
"@playwright/test"
;
const
testWithLastVersion
=
testWithConfig
({
preLaunchHook
:
async
({
userDataDir
})
=>
{
fs
.
mkdirSync
(
path
.
join
(
userDataDir
),
{
recursive
:
true
});
fs
.
writeFileSync
(
path
.
join
(
userDataDir
,
".last_version"
),
"0.1.0"
);
fs
.
copyFileSync
(
path
.
join
(
__dirname
,
"fixtures"
,
"backups"
,
"empty-v0.12.0-beta.1.db"
),
path
.
join
(
userDataDir
,
"sqlite.db"
),
);
},
});
const
testWithMultipleBackups
=
testWithConfig
({
preLaunchHook
:
async
({
userDataDir
})
=>
{
fs
.
mkdirSync
(
path
.
join
(
userDataDir
),
{
recursive
:
true
});
// Make sure there's a last version file so the version upgrade is detected.
fs
.
writeFileSync
(
path
.
join
(
userDataDir
,
".last_version"
),
"0.1.0"
);
// Create backups directory
const
backupsDir
=
path
.
join
(
userDataDir
,
"backups"
);
fs
.
mkdirSync
(
backupsDir
,
{
recursive
:
true
});
// Create 5 mock backup directories with different timestamps
// These timestamps are in ascending order (oldest to newest)
const
mockBackups
=
[
{
name
:
"v1.0.0_2023-01-01T10-00-00-000Z_upgrade_from_0.9.0"
,
timestamp
:
"2023-01-01T10:00:00.000Z"
,
version
:
"1.0.0"
,
reason
:
"upgrade_from_0.9.0"
,
},
{
name
:
"v1.0.1_2023-01-02T10-00-00-000Z_upgrade_from_1.0.0"
,
timestamp
:
"2023-01-02T10:00:00.000Z"
,
version
:
"1.0.1"
,
reason
:
"upgrade_from_1.0.0"
,
},
{
name
:
"v1.0.2_2023-01-03T10-00-00-000Z_upgrade_from_1.0.1"
,
timestamp
:
"2023-01-03T10:00:00.000Z"
,
version
:
"1.0.2"
,
reason
:
"upgrade_from_1.0.1"
,
},
{
name
:
"v1.0.3_2023-01-04T10-00-00-000Z_upgrade_from_1.0.2"
,
timestamp
:
"2023-01-04T10:00:00.000Z"
,
version
:
"1.0.3"
,
reason
:
"upgrade_from_1.0.2"
,
},
{
name
:
"v1.0.4_2023-01-05T10-00-00-000Z_upgrade_from_1.0.3"
,
timestamp
:
"2023-01-05T10:00:00.000Z"
,
version
:
"1.0.4"
,
reason
:
"upgrade_from_1.0.3"
,
},
];
// Create each backup directory with realistic structure
for
(
const
backup
of
mockBackups
)
{
const
backupPath
=
path
.
join
(
backupsDir
,
backup
.
name
);
fs
.
mkdirSync
(
backupPath
,
{
recursive
:
true
});
// Create backup metadata
const
metadata
=
{
version
:
backup
.
version
,
timestamp
:
backup
.
timestamp
,
reason
:
backup
.
reason
,
files
:
{
settings
:
true
,
database
:
true
,
},
checksums
:
{
settings
:
"mock_settings_checksum_"
+
backup
.
version
,
database
:
"mock_database_checksum_"
+
backup
.
version
,
},
};
fs
.
writeFileSync
(
path
.
join
(
backupPath
,
"backup.json"
),
JSON
.
stringify
(
metadata
,
null
,
2
),
);
// Create mock backup files
fs
.
writeFileSync
(
path
.
join
(
backupPath
,
"user-settings.json"
),
JSON
.
stringify
({
version
:
backup
.
version
,
mockData
:
true
},
null
,
2
),
);
fs
.
writeFileSync
(
path
.
join
(
backupPath
,
"sqlite.db"
),
`mock_database_content_
${
backup
.
version
}
`
,
);
}
},
});
const
ensureAppIsRunning
=
async
(
po
:
PageObject
)
=>
{
await
po
.
page
.
waitForSelector
(
"h1"
);
const
text
=
await
po
.
page
.
$eval
(
"h1"
,
(
el
)
=>
el
.
textContent
);
expect
(
text
).
toBe
(
"Build your dream app"
);
};
test
(
"backup is not created for first run"
,
async
({
po
})
=>
{
await
ensureAppIsRunning
(
po
);
expect
(
fs
.
existsSync
(
path
.
join
(
po
.
userDataDir
,
"backups"
))).
toEqual
(
false
);
});
testWithLastVersion
(
"backup is created if version is upgraded"
,
async
({
po
})
=>
{
await
ensureAppIsRunning
(
po
);
const
backups
=
fs
.
readdirSync
(
path
.
join
(
po
.
userDataDir
,
"backups"
));
expect
(
backups
).
toHaveLength
(
1
);
const
backupDir
=
path
.
join
(
po
.
userDataDir
,
"backups"
,
backups
[
0
]);
const
backupMetadata
=
JSON
.
parse
(
fs
.
readFileSync
(
path
.
join
(
backupDir
,
"backup.json"
),
"utf8"
),
);
expect
(
backupMetadata
.
version
).
toBeDefined
();
expect
(
backupMetadata
.
timestamp
).
toBeDefined
();
expect
(
backupMetadata
.
reason
).
toBe
(
"upgrade_from_0.1.0"
);
expect
(
backupMetadata
.
files
.
settings
).
toBe
(
true
);
expect
(
backupMetadata
.
files
.
database
).
toBe
(
true
);
expect
(
backupMetadata
.
checksums
.
settings
).
toBeDefined
();
expect
(
backupMetadata
.
checksums
.
database
).
toBeDefined
();
// Compare the backup files to the original files
const
originalSettings
=
fs
.
readFileSync
(
path
.
join
(
po
.
userDataDir
,
"user-settings.json"
),
"utf8"
,
);
const
backupSettings
=
fs
.
readFileSync
(
path
.
join
(
backupDir
,
"user-settings.json"
),
"utf8"
,
);
expect
(
cleanSettings
(
backupSettings
)).
toEqual
(
cleanSettings
(
originalSettings
),
);
// For database, verify the backup file exists and has correct checksum
const
backupDbPath
=
path
.
join
(
backupDir
,
"sqlite.db"
);
const
originalDbPath
=
path
.
join
(
po
.
userDataDir
,
"sqlite.db"
);
expect
(
fs
.
existsSync
(
backupDbPath
)).
toBe
(
true
);
expect
(
fs
.
existsSync
(
originalDbPath
)).
toBe
(
true
);
const
backupChecksum
=
calculateChecksum
(
backupDbPath
);
// Verify backup metadata contains the correct checksum
expect
(
backupMetadata
.
checksums
.
database
).
toBe
(
backupChecksum
);
},
);
testWithMultipleBackups
(
"backup cleanup deletes oldest backups when exceeding MAX_BACKUPS"
,
async
({
po
})
=>
{
await
ensureAppIsRunning
(
po
);
const
backupsDir
=
path
.
join
(
po
.
userDataDir
,
"backups"
);
const
backups
=
fs
.
readdirSync
(
backupsDir
);
// Should have only 3 backups remaining (MAX_BACKUPS = 3)
expect
(
backups
).
toHaveLength
(
3
);
const
expectedRemainingBackups
=
[
"*"
,
// These are the two older backups
"v1.0.4_2023-01-05T10-00-00-000Z_upgrade_from_1.0.3"
,
"v1.0.3_2023-01-04T10-00-00-000Z_upgrade_from_1.0.2"
,
];
// Check that the expected backups exist
for
(
let
backup
of
expectedRemainingBackups
)
{
let
expectedBackup
=
backup
;
if
(
backup
===
"*"
)
{
expectedBackup
=
backups
[
0
];
expect
(
expectedBackup
.
endsWith
(
"_upgrade_from_0.1.0"
)).
toEqual
(
true
);
}
else
{
expect
(
backups
).
toContain
(
expectedBackup
);
}
// Verify the backup directory and metadata still exist
const
backupPath
=
path
.
join
(
backupsDir
,
expectedBackup
);
expect
(
fs
.
existsSync
(
backupPath
)).
toBe
(
true
);
expect
(
fs
.
existsSync
(
path
.
join
(
backupPath
,
"backup.json"
))).
toBe
(
true
);
expect
(
fs
.
existsSync
(
path
.
join
(
backupPath
,
"user-settings.json"
))).
toBe
(
true
,
);
// The first backup does NOT have a SQLite database because the backup
// manager is run before the DB is initialized.
expect
(
fs
.
existsSync
(
path
.
join
(
backupPath
,
"sqlite.db"
))).
toBe
(
backup
!==
"*"
,
);
}
// The 2 oldest backups should have been deleted
const
deletedBackups
=
[
"v1.0.0_2023-01-01T10-00-00-000Z_upgrade_from_0.9.0"
,
// oldest
"v1.0.1_2023-01-02T10-00-00-000Z_upgrade_from_1.0.0"
,
// second oldest
"v1.0.2_2023-01-03T10-00-00-000Z_upgrade_from_1.0.1"
,
// third oldest
];
for
(
const
deletedBackup
of
deletedBackups
)
{
expect
(
backups
).
not
.
toContain
(
deletedBackup
);
expect
(
fs
.
existsSync
(
path
.
join
(
backupsDir
,
deletedBackup
))).
toBe
(
false
);
}
},
);
function
cleanSettings
(
settings
:
string
)
{
const
parsed
=
JSON
.
parse
(
settings
);
delete
parsed
.
hasRunBefore
;
delete
parsed
.
isTestMode
;
delete
parsed
.
lastShownReleaseNotesVersion
;
return
parsed
;
}
function
calculateChecksum
(
filePath
:
string
):
string
{
const
fileBuffer
=
fs
.
readFileSync
(
filePath
);
const
hash
=
crypto
.
createHash
(
"sha256"
);
hash
.
update
(
fileBuffer
);
return
hash
.
digest
(
"hex"
);
}
e2e-tests/fixtures/backups/empty-v0.12.0-beta.1.db
0 → 100644
浏览文件 @
dfdd267f
File added
e2e-tests/helpers/test_helper.ts
浏览文件 @
dfdd267f
...
...
@@ -187,7 +187,7 @@ class GitHubConnector {
}
export
class
PageObject
{
p
rivate
userDataDir
:
string
;
p
ublic
userDataDir
:
string
;
public
githubConnector
:
GitHubConnector
;
constructor
(
public
electronApp
:
ElectronApplication
,
...
...
@@ -935,15 +935,27 @@ export class PageObject {
}
}
interface
ElectronConfig
{
preLaunchHook
?:
({
userDataDir
}:
{
userDataDir
:
string
})
=>
Promise
<
void
>
;
}
// From https://github.com/microsoft/playwright/issues/8208#issuecomment-1435475930
//
// Note how we mark the fixture as { auto: true }.
// This way it is always instantiated, even if the test does not use it explicitly.
export
const
test
=
base
.
extend
<
{
electronConfig
:
ElectronConfig
;
attachScreenshotsToReport
:
void
;
electronApp
:
ElectronApplication
;
po
:
PageObject
;
}
>
({
electronConfig
:
[
async
({},
use
)
=>
{
// Default configuration - tests can override this fixture
await
use
({});
},
{
auto
:
true
},
],
po
:
[
async
({
electronApp
},
use
)
=>
{
const
page
=
await
electronApp
.
firstWindow
();
...
...
@@ -976,7 +988,7 @@ export const test = base.extend<{
{
auto
:
true
},
],
electronApp
:
[
async
({},
use
)
=>
{
async
({
electronConfig
},
use
)
=>
{
// find the latest build in the out directory
const
latestBuild
=
eph
.
findLatestBuild
();
// parse the directory and find paths and other info
...
...
@@ -990,15 +1002,15 @@ export const test = base.extend<{
// This is just a hack to avoid the AI setup screen.
process
.
env
.
OPENAI_API_KEY
=
"sk-test"
;
const
baseTmpDir
=
os
.
tmpdir
();
const
USER_DATA_DIR
=
path
.
join
(
baseTmpDir
,
`dyad-e2e-tests-
${
Date
.
now
()}
`
,
);
const
userDataDir
=
path
.
join
(
baseTmpDir
,
`dyad-e2e-tests-
${
Date
.
now
()}
`
);
if
(
electronConfig
.
preLaunchHook
)
{
await
electronConfig
.
preLaunchHook
({
userDataDir
});
}
const
electronApp
=
await
electron
.
launch
({
args
:
[
appInfo
.
main
,
"--enable-logging"
,
`--user-data-dir=
${
USER_DATA_DIR
}
`
,
`--user-data-dir=
${
userDataDir
}
`
,
],
executablePath
:
appInfo
.
executable
,
// Strong suspicion this is causing issues on Windows with tests hanging due to error:
...
...
@@ -1007,7 +1019,7 @@ export const test = base.extend<{
// dir: "test-results",
// },
});
(
electronApp
as
any
).
$dyadUserDataDir
=
USER_DATA_DIR
;
(
electronApp
as
any
).
$dyadUserDataDir
=
userDataDir
;
console
.
log
(
"electronApp launched!"
);
if
(
showDebugLogs
)
{
...
...
@@ -1064,6 +1076,14 @@ export const test = base.extend<{
],
});
export
function
testWithConfig
(
config
:
ElectronConfig
)
{
return
test
.
extend
({
electronConfig
:
async
({},
use
)
=>
{
await
use
(
config
);
},
});
}
// Wrapper that skips tests on Windows platform
export
const
testSkipIfWindows
=
os
.
platform
()
===
"win32"
?
test
.
skip
:
test
;
...
...
e2e-tests/snapshots/problems.spec.ts_problems---manual-edit-next-js-1.aria.yml
浏览文件 @
dfdd267f
-
img
-
text
:
1 error
-
button "R
echeck
"
:
-
button "R
un checks
"
:
-
img
-
button "Fix All"
:
-
img
...
...
e2e-tests/snapshots/problems.spec.ts_problems---manual-edit-next-js-2.aria.yml
浏览文件 @
dfdd267f
-
paragraph
:
No problems found
-
button "Recheck"
:
-
img
-
button "Run checks"
:
-
img
\ No newline at end of file
e2e-tests/snapshots/problems.spec.ts_problems---manual-edit-react-vite-1.aria.yml
浏览文件 @
dfdd267f
-
img
-
text
:
1 error
-
button "R
echeck
"
:
-
button "R
un checks
"
:
-
img
-
button "Fix All"
:
-
img
...
...
e2e-tests/snapshots/problems.spec.ts_problems---manual-edit-react-vite-2.aria.yml
浏览文件 @
dfdd267f
-
paragraph
:
No problems found
-
button "Recheck"
:
-
img
-
button "Run checks"
:
-
img
\ No newline at end of file
src/backup_manager.ts
0 → 100644
浏览文件 @
dfdd267f
差异被折叠。
点击展开。
src/db/index.ts
浏览文件 @
dfdd267f
// db.ts
import
{
type
BetterSQLite3Database
,
drizzle
,
...
...
@@ -8,7 +9,6 @@ import { migrate } from "drizzle-orm/better-sqlite3/migrator";
import
path
from
"node:path"
;
import
fs
from
"node:fs"
;
import
{
getDyadAppPath
,
getUserDataPath
}
from
"../paths/paths"
;
import
log
from
"electron-log"
;
const
logger
=
log
.
scope
(
"db"
);
...
...
@@ -36,10 +36,8 @@ export function initializeDatabase(): BetterSQLite3Database<typeof schema> & {
// Check if the database file exists and remove it if it has issues
try
{
// If the file exists but is empty or corrupted, it might cause issues
if
(
fs
.
existsSync
(
dbPath
))
{
const
stats
=
fs
.
statSync
(
dbPath
);
// If the file is very small, it might be corrupted
if
(
stats
.
size
<
100
)
{
logger
.
log
(
"Database file exists but may be corrupted. Removing it..."
);
fs
.
unlinkSync
(
dbPath
);
...
...
@@ -50,16 +48,11 @@ export function initializeDatabase(): BetterSQLite3Database<typeof schema> & {
}
fs
.
mkdirSync
(
getUserDataPath
(),
{
recursive
:
true
});
// Just a convenient time to create it.
fs
.
mkdirSync
(
getDyadAppPath
(
"."
),
{
recursive
:
true
});
// Open the database with a higher timeout
const
sqlite
=
new
Database
(
dbPath
,
{
timeout
:
10000
});
// Enable foreign key constraints
sqlite
.
pragma
(
"foreign_keys = ON"
);
// Create DB instance with schema
_db
=
drizzle
(
sqlite
,
{
schema
});
try
{
...
...
@@ -77,13 +70,25 @@ export function initializeDatabase(): BetterSQLite3Database<typeof schema> & {
return
_db
as
any
;
}
// Initialize database on import
try
{
initializeDatabase
();
}
catch
(
error
)
{
logger
.
error
(
"Failed to initialize database:"
,
error
);
/**
* Get the database instance (throws if not initialized)
*/
export
function
getDb
():
BetterSQLite3Database
<
typeof
schema
>
&
{
$client
:
Database
.
Database
;
}
{
if
(
!
_db
)
{
throw
new
Error
(
"Database not initialized. Call initializeDatabase() first."
,
);
}
return
_db
as
any
;
}
export
const
db
=
_db
as
any
as
BetterSQLite3Database
<
typeof
schema
>
&
{
export
const
db
=
new
Proxy
({}
as
any
,
{
get
(
target
,
prop
)
{
const
database
=
getDb
();
return
database
[
prop
as
keyof
typeof
database
];
},
})
as
BetterSQLite3Database
<
typeof
schema
>
&
{
$client
:
Database
.
Database
;
};
src/main.ts
浏览文件 @
dfdd267f
...
...
@@ -6,10 +6,16 @@ import dotenv from "dotenv";
import
started
from
"electron-squirrel-startup"
;
import
{
updateElectronApp
,
UpdateSourceType
}
from
"update-electron-app"
;
import
log
from
"electron-log"
;
import
{
readSettings
,
writeSettings
}
from
"./main/settings"
;
import
{
getSettingsFilePath
,
readSettings
,
writeSettings
,
}
from
"./main/settings"
;
import
{
handleSupabaseOAuthReturn
}
from
"./supabase_admin/supabase_return_handler"
;
import
{
handleDyadProReturn
}
from
"./main/pro"
;
import
{
IS_TEST_BUILD
}
from
"./ipc/utils/test_utils"
;
import
{
BackupManager
}
from
"./backup_manager"
;
import
{
getDatabasePath
,
initializeDatabase
}
from
"./db"
;
log
.
errorHandler
.
startCatching
();
log
.
eventLogger
.
startLogging
();
...
...
@@ -58,11 +64,20 @@ if (process.defaultApp) {
}
export
async
function
onReady
()
{
try
{
const
backupManager
=
new
BackupManager
({
settingsFile
:
getSettingsFilePath
(),
dbFile
:
getDatabasePath
(),
});
await
backupManager
.
initialize
();
}
catch
(
e
)
{
logger
.
error
(
"Error initializing backup manager"
,
e
);
}
initializeDatabase
();
await
onFirstRunMaybe
();
createWindow
();
}
app
.
whenReady
().
then
(
onReady
);
/**
* Is this the first run of Fiddle? If so, perform
* tasks that we only want to do in this case.
...
...
@@ -164,11 +179,7 @@ if (!gotTheLock) {
// the commandLine is array of strings in which last element is deep link url
handleDeepLinkReturn
(
commandLine
.
pop
()
!
);
});
// Create mainWindow, load the rest of the app, etc...
app
.
whenReady
().
then
(()
=>
{
createWindow
();
});
app
.
whenReady
().
then
(
onReady
);
}
// Handle the protocol. In this case, we choose to show an Error Box.
...
...
src/main/settings.ts
浏览文件 @
dfdd267f
...
...
@@ -27,7 +27,7 @@ const DEFAULT_SETTINGS: UserSettings = {
const
SETTINGS_FILE
=
"user-settings.json"
;
function
getSettingsFilePath
():
string
{
export
function
getSettingsFilePath
():
string
{
return
path
.
join
(
getUserDataPath
(),
SETTINGS_FILE
);
}
...
...
编写
预览
Markdown
格式
0%
重试
或
添加新文件
添加附件
取消
您添加了
0
人
到此讨论。请谨慎行事。
请先完成此评论的编辑!
取消
请
注册
或者
登录
后发表评论