@coreframe/scripts 0.0.0 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/coreframe.js +35 -0
- package/config.test.ts +15 -0
- package/config.ts +9 -0
- package/coreframe.ts +272 -0
- package/db/client.ts +72 -0
- package/db/config.ts +41 -0
- package/db/drizzle-kit.test.ts +27 -0
- package/db/drizzle-kit.ts +39 -0
- package/db/ensure.ts +67 -0
- package/db/migrate-held.test.ts +81 -0
- package/db/migrate-held.ts +166 -0
- package/db/migrate.ts +39 -0
- package/db/project.ts +28 -0
- package/db/reset.ts +24 -0
- package/db/seed.ts +38 -0
- package/db/setup.ts +18 -0
- package/deploy/check-migrations.ts +31 -0
- package/deploy/checks.ts +38 -0
- package/deploy/config.test.ts +24 -0
- package/deploy/config.ts +68 -0
- package/deploy/migrations.test.ts +110 -0
- package/deploy/migrations.ts +317 -0
- package/deploy/pending-migrations.test.ts +93 -0
- package/deploy/pending-migrations.ts +45 -0
- package/deploy/tauri-release.test.ts +196 -0
- package/deploy/tauri-release.ts +816 -0
- package/deploy/version.test.ts +29 -0
- package/deploy/version.ts +65 -0
- package/deploy.test.ts +101 -0
- package/deploy.ts +346 -0
- package/dev.test.ts +153 -0
- package/dev.ts +155 -0
- package/dist/config.d.ts +5 -0
- package/dist/config.js +3 -0
- package/dist/coreframe.d.ts +280 -0
- package/dist/coreframe.js +240 -0
- package/dist/db/client.d.ts +22 -0
- package/dist/db/client.js +29 -0
- package/dist/db/config.d.ts +21 -0
- package/dist/db/config.js +32 -0
- package/dist/db/drizzle-kit.d.ts +8 -0
- package/dist/db/drizzle-kit.js +22 -0
- package/dist/db/ensure.d.ts +2 -0
- package/dist/db/ensure.js +40 -0
- package/dist/db/migrate-held.d.ts +12 -0
- package/dist/db/migrate-held.js +119 -0
- package/dist/db/migrate.d.ts +2 -0
- package/dist/db/migrate.js +25 -0
- package/dist/db/project.d.ts +8 -0
- package/dist/db/project.js +9 -0
- package/dist/db/reset.d.ts +2 -0
- package/dist/db/reset.js +19 -0
- package/dist/db/seed.d.ts +3 -0
- package/dist/db/seed.js +23 -0
- package/dist/db/setup.d.ts +3 -0
- package/dist/db/setup.js +15 -0
- package/dist/deploy/check-migrations.d.ts +2 -0
- package/dist/deploy/check-migrations.js +18 -0
- package/dist/deploy/checks.d.ts +2 -0
- package/dist/deploy/checks.js +27 -0
- package/dist/deploy/config.d.ts +50 -0
- package/dist/deploy/config.js +9 -0
- package/dist/deploy/migrations.d.ts +21 -0
- package/dist/deploy/migrations.js +203 -0
- package/dist/deploy/pending-migrations.d.ts +5 -0
- package/dist/deploy/pending-migrations.js +30 -0
- package/dist/deploy/tauri-release.d.ts +85 -0
- package/dist/deploy/tauri-release.js +512 -0
- package/dist/deploy/version.d.ts +18 -0
- package/dist/deploy/version.js +41 -0
- package/dist/deploy.d.ts +17 -0
- package/dist/deploy.js +247 -0
- package/dist/dev.d.ts +13 -0
- package/dist/dev.js +96 -0
- package/dist/image-generator.d.ts +2 -0
- package/dist/image-generator.js +59 -0
- package/dist/project.d.ts +1 -0
- package/dist/project.js +13 -0
- package/dist/typecheck.d.ts +2 -0
- package/dist/typecheck.js +45 -0
- package/dist/upgrade/check-skill-current.d.ts +6 -0
- package/dist/upgrade/check-skill-current.js +88 -0
- package/dist/upgrade/ensure-template-remote.d.ts +6 -0
- package/dist/upgrade/ensure-template-remote.js +57 -0
- package/dist/upgrade/fast-forward-template-files.d.ts +7 -0
- package/dist/upgrade/fast-forward-template-files.js +280 -0
- package/image-generator.ts +84 -0
- package/package.json +48 -8
- package/project.ts +6 -0
- package/tsconfig.build.json +12 -0
- package/tsconfig.json +20 -0
- package/typecheck.ts +55 -0
- package/upgrade/check-skill-current.ts +112 -0
- package/upgrade/ensure-template-remote.ts +79 -0
- package/upgrade/fast-forward-template-files.ts +408 -0
- package/readme.md +0 -1
|
@@ -0,0 +1,816 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto"
|
|
2
|
+
import { constants } from "node:fs"
|
|
3
|
+
import { access, cp, mkdir, readFile, rm, stat, writeFile } from "node:fs/promises"
|
|
4
|
+
import { basename, dirname, join, relative, resolve, sep } from "node:path"
|
|
5
|
+
import { setTimeout as delay } from "node:timers/promises"
|
|
6
|
+
|
|
7
|
+
import nanoSpawn from "nano-spawn"
|
|
8
|
+
import { glob } from "tinyglobby"
|
|
9
|
+
|
|
10
|
+
import type { TauriReleaseConfig, TauriReleasePlatform } from "./config.ts"
|
|
11
|
+
import { tauriReleasePlatforms } from "./config.ts"
|
|
12
|
+
|
|
13
|
+
export type TauriReleaseArgs = {
|
|
14
|
+
tauriLocalPlatforms: TauriReleasePlatform[]
|
|
15
|
+
tauriWorkflowPlatforms: TauriReleasePlatform[]
|
|
16
|
+
tauriWorkflowName?: string
|
|
17
|
+
tauriWorkflowRef?: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type TauriReleasePlan = {
|
|
21
|
+
artifactDownloadDir: string
|
|
22
|
+
b2ApplicationKey?: string
|
|
23
|
+
b2ApplicationKeyId?: string
|
|
24
|
+
b2Bucket?: string
|
|
25
|
+
enabled: boolean
|
|
26
|
+
latestManifestPath: string
|
|
27
|
+
localPlatforms: TauriReleasePlatform[]
|
|
28
|
+
platform: Record<TauriReleasePlatform, ResolvedTauriReleasePlatformConfig>
|
|
29
|
+
publicBaseUrl?: string
|
|
30
|
+
publicPathPrefix: string
|
|
31
|
+
releaseNotesPath?: string
|
|
32
|
+
releasePath: string
|
|
33
|
+
releaseUploadDir: string
|
|
34
|
+
signingPrivateKey?: string
|
|
35
|
+
workflowName: string
|
|
36
|
+
workflowPlatforms: TauriReleasePlatform[]
|
|
37
|
+
workflowPollIntervalMs: number
|
|
38
|
+
workflowRef?: string
|
|
39
|
+
workflowTimeoutMs: number
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export type TauriReleaseMetadataSnapshot = {
|
|
43
|
+
files: { path: string; source: string }[]
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export type PreparedTauriRelease = {
|
|
47
|
+
manifestPath: string
|
|
48
|
+
uploadDir: string
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
type ResolvedTauriReleasePlatformConfig = {
|
|
52
|
+
artifactGlobs: string[]
|
|
53
|
+
buildArgs: string[]
|
|
54
|
+
targetTriple: string
|
|
55
|
+
updatePlatform: string
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
type TauriUpdateManifest = {
|
|
59
|
+
notes?: string
|
|
60
|
+
platforms: Record<string, { signature: string; url: string }>
|
|
61
|
+
pub_date: string
|
|
62
|
+
version: string
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const defaultWorkflowName = "tauri-release-artifacts.yml"
|
|
66
|
+
const defaultWorkflowPollIntervalMs = 15_000
|
|
67
|
+
const defaultWorkflowTimeoutMs = 60 * 60_000
|
|
68
|
+
|
|
69
|
+
const defaultPlatformConfig: Record<TauriReleasePlatform, ResolvedTauriReleasePlatformConfig> = {
|
|
70
|
+
android: {
|
|
71
|
+
artifactGlobs: ["tauri/src-tauri/target/**/*.sig", "tauri/gen/android/**/*.sig"],
|
|
72
|
+
buildArgs: ["tauri", "android", "build", "--target", "aarch64-linux-android"],
|
|
73
|
+
targetTriple: "aarch64-linux-android",
|
|
74
|
+
updatePlatform: "android-aarch64",
|
|
75
|
+
},
|
|
76
|
+
ios: {
|
|
77
|
+
artifactGlobs: ["tauri/src-tauri/target/**/*.sig", "tauri/gen/apple/**/*.sig"],
|
|
78
|
+
buildArgs: ["tauri", "ios", "build", "--target", "aarch64-apple-ios"],
|
|
79
|
+
targetTriple: "aarch64-apple-ios",
|
|
80
|
+
updatePlatform: "ios-aarch64",
|
|
81
|
+
},
|
|
82
|
+
linux: {
|
|
83
|
+
artifactGlobs: [
|
|
84
|
+
"tauri/src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/**/*.sig",
|
|
85
|
+
"tauri/src-tauri/target/release/bundle/**/*.sig",
|
|
86
|
+
"tauri/src-tauri/target/**/release/bundle/**/*.sig",
|
|
87
|
+
],
|
|
88
|
+
buildArgs: ["tauri", "build", "--target", "x86_64-unknown-linux-gnu", "--", "--locked"],
|
|
89
|
+
targetTriple: "x86_64-unknown-linux-gnu",
|
|
90
|
+
updatePlatform: "linux-x86_64",
|
|
91
|
+
},
|
|
92
|
+
macos: {
|
|
93
|
+
artifactGlobs: [
|
|
94
|
+
"tauri/src-tauri/target/aarch64-apple-darwin/release/bundle/**/*.sig",
|
|
95
|
+
"tauri/src-tauri/target/release/bundle/**/*.sig",
|
|
96
|
+
"tauri/src-tauri/target/**/release/bundle/**/*.sig",
|
|
97
|
+
],
|
|
98
|
+
buildArgs: ["tauri", "build", "--target", "aarch64-apple-darwin", "--", "--locked"],
|
|
99
|
+
targetTriple: "aarch64-apple-darwin",
|
|
100
|
+
updatePlatform: "darwin-aarch64",
|
|
101
|
+
},
|
|
102
|
+
windows: {
|
|
103
|
+
artifactGlobs: [
|
|
104
|
+
"tauri/src-tauri/target/x86_64-pc-windows-msvc/release/bundle/**/*.sig",
|
|
105
|
+
"tauri/src-tauri/target/release/bundle/**/*.sig",
|
|
106
|
+
"tauri/src-tauri/target/**/release/bundle/**/*.sig",
|
|
107
|
+
],
|
|
108
|
+
buildArgs: ["tauri", "build", "--target", "x86_64-pc-windows-msvc", "--", "--locked"],
|
|
109
|
+
targetTriple: "x86_64-pc-windows-msvc",
|
|
110
|
+
updatePlatform: "windows-x86_64",
|
|
111
|
+
},
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function parseTauriReleasePlatformList(value: string | undefined) {
|
|
115
|
+
if (!value) {
|
|
116
|
+
return []
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return value
|
|
120
|
+
.split(/[,\s]+/u)
|
|
121
|
+
.filter(Boolean)
|
|
122
|
+
.map((platform) => parseTauriReleasePlatform(platform))
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function parseTauriReleasePlatform(value: string): TauriReleasePlatform {
|
|
126
|
+
if (isTauriReleasePlatform(value)) {
|
|
127
|
+
return value
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
throw new Error(
|
|
131
|
+
`Invalid Tauri release platform "${value}". Expected one of: ${tauriReleasePlatforms.join(", ")}.`,
|
|
132
|
+
)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function resolveTauriReleasePlan({
|
|
136
|
+
args,
|
|
137
|
+
config = {},
|
|
138
|
+
env,
|
|
139
|
+
}: {
|
|
140
|
+
args: TauriReleaseArgs
|
|
141
|
+
config?: TauriReleaseConfig
|
|
142
|
+
env: NodeJS.ProcessEnv
|
|
143
|
+
}): TauriReleasePlan {
|
|
144
|
+
const localPlatforms = uniquePlatforms([
|
|
145
|
+
...(config.localPlatforms ?? []),
|
|
146
|
+
...parseTauriReleasePlatformList(env.TAURI_RELEASE_LOCAL_PLATFORMS),
|
|
147
|
+
...args.tauriLocalPlatforms,
|
|
148
|
+
])
|
|
149
|
+
const workflowPlatforms = uniquePlatforms([
|
|
150
|
+
...(config.workflowPlatforms ?? []),
|
|
151
|
+
...parseTauriReleasePlatformList(env.TAURI_RELEASE_WORKFLOW_PLATFORMS),
|
|
152
|
+
...args.tauriWorkflowPlatforms,
|
|
153
|
+
])
|
|
154
|
+
const duplicatedPlatforms = localPlatforms.filter((platform) =>
|
|
155
|
+
workflowPlatforms.includes(platform),
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
if (duplicatedPlatforms.length > 0) {
|
|
159
|
+
throw new Error(
|
|
160
|
+
`Build each Tauri release platform either locally or through the workflow, not both: ${duplicatedPlatforms.join(", ")}.`,
|
|
161
|
+
)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
artifactDownloadDir:
|
|
166
|
+
env.TAURI_RELEASE_ARTIFACT_DOWNLOAD_DIR ??
|
|
167
|
+
config.artifactDownloadDir ??
|
|
168
|
+
".coreframe/tauri-artifacts",
|
|
169
|
+
b2ApplicationKey: env.B2_APPLICATION_KEY ?? env.B2_SECRET_ACCESS_KEY,
|
|
170
|
+
b2ApplicationKeyId: env.B2_APPLICATION_KEY_ID ?? env.B2_KEY_ID ?? env.B2_ACCESS_KEY_ID,
|
|
171
|
+
b2Bucket: config.b2Bucket ?? env.B2_BUCKET,
|
|
172
|
+
enabled: localPlatforms.length > 0 || workflowPlatforms.length > 0,
|
|
173
|
+
latestManifestPath:
|
|
174
|
+
env.TAURI_RELEASE_LATEST_MANIFEST_PATH ?? config.latestManifestPath ?? "latest.json",
|
|
175
|
+
localPlatforms,
|
|
176
|
+
platform: resolvePlatformConfig(config.platform, env),
|
|
177
|
+
publicBaseUrl:
|
|
178
|
+
config.publicBaseUrl ?? env.TAURI_RELEASE_PUBLIC_BASE_URL ?? env.UPDATE_PUBLIC_BASE_URL,
|
|
179
|
+
publicPathPrefix:
|
|
180
|
+
env.TAURI_RELEASE_PUBLIC_PATH_PREFIX ??
|
|
181
|
+
config.publicPathPrefix ??
|
|
182
|
+
env.UPDATE_PUBLIC_PATH_PREFIX ??
|
|
183
|
+
"",
|
|
184
|
+
releaseNotesPath: env.TAURI_RELEASE_NOTES_FILE ?? config.releaseNotesPath,
|
|
185
|
+
releasePath: env.TAURI_RELEASE_PATH ?? config.releasePath ?? "releases/${version}",
|
|
186
|
+
releaseUploadDir: env.TAURI_RELEASE_UPLOAD_DIR ?? config.releaseUploadDir ?? "release-upload",
|
|
187
|
+
signingPrivateKey: env.TAURI_SIGNING_PRIVATE_KEY,
|
|
188
|
+
workflowName:
|
|
189
|
+
args.tauriWorkflowName ??
|
|
190
|
+
env.TAURI_RELEASE_WORKFLOW_NAME ??
|
|
191
|
+
config.workflowName ??
|
|
192
|
+
defaultWorkflowName,
|
|
193
|
+
workflowPlatforms,
|
|
194
|
+
workflowPollIntervalMs:
|
|
195
|
+
parsePositiveInteger(
|
|
196
|
+
env.TAURI_RELEASE_WORKFLOW_POLL_SECONDS,
|
|
197
|
+
"TAURI_RELEASE_WORKFLOW_POLL_SECONDS",
|
|
198
|
+
) * 1000 ||
|
|
199
|
+
config.workflowPollIntervalMs ||
|
|
200
|
+
defaultWorkflowPollIntervalMs,
|
|
201
|
+
workflowRef: args.tauriWorkflowRef ?? env.TAURI_RELEASE_WORKFLOW_REF ?? config.workflowRef,
|
|
202
|
+
workflowTimeoutMs:
|
|
203
|
+
parsePositiveInteger(
|
|
204
|
+
env.TAURI_RELEASE_WORKFLOW_TIMEOUT_MINUTES,
|
|
205
|
+
"TAURI_RELEASE_WORKFLOW_TIMEOUT_MINUTES",
|
|
206
|
+
) * 60_000 ||
|
|
207
|
+
config.workflowTimeoutMs ||
|
|
208
|
+
defaultWorkflowTimeoutMs,
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export async function validateTauriReleasePlan(plan: TauriReleasePlan, cwd = process.cwd()) {
|
|
213
|
+
if (!plan.enabled) {
|
|
214
|
+
return
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const missing = [
|
|
218
|
+
["TAURI_RELEASE_PUBLIC_BASE_URL", plan.publicBaseUrl],
|
|
219
|
+
["B2_APPLICATION_KEY_ID", plan.b2ApplicationKeyId],
|
|
220
|
+
["B2_APPLICATION_KEY", plan.b2ApplicationKey],
|
|
221
|
+
["B2_BUCKET", plan.b2Bucket],
|
|
222
|
+
].flatMap(([name, value]) => (value ? [] : [name]))
|
|
223
|
+
|
|
224
|
+
if (missing.length > 0) {
|
|
225
|
+
throw new Error(`Missing required Tauri release configuration: ${missing.join(", ")}`)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
try {
|
|
229
|
+
new URL(plan.publicBaseUrl as string)
|
|
230
|
+
} catch {
|
|
231
|
+
throw new Error(`Invalid TAURI_RELEASE_PUBLIC_BASE_URL: ${plan.publicBaseUrl}`)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const tauriConfigPath = join(cwd, "tauri/src-tauri/tauri.conf.json")
|
|
235
|
+
|
|
236
|
+
if (!(await pathExists(tauriConfigPath))) {
|
|
237
|
+
throw new Error(
|
|
238
|
+
"Tauri release platforms were requested, but tauri/src-tauri/tauri.conf.json does not exist.",
|
|
239
|
+
)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (plan.localPlatforms.length > 0 && !plan.signingPrivateKey) {
|
|
243
|
+
throw new Error("TAURI_SIGNING_PRIVATE_KEY is required for local Tauri release builds.")
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (plan.workflowPlatforms.length > 0) {
|
|
247
|
+
await nanoSpawn(
|
|
248
|
+
"gh",
|
|
249
|
+
["workflow", "view", plan.workflowName, "--ref", plan.workflowRef ?? (await currentGitRef())],
|
|
250
|
+
{
|
|
251
|
+
stdin: "ignore",
|
|
252
|
+
},
|
|
253
|
+
)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
await nanoSpawn("b2", ["version"], { stdin: "ignore" })
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export async function writeTauriReleaseMetadata(
|
|
260
|
+
plan: TauriReleasePlan,
|
|
261
|
+
version: string,
|
|
262
|
+
cwd = process.cwd(),
|
|
263
|
+
): Promise<TauriReleaseMetadataSnapshot> {
|
|
264
|
+
if (!plan.enabled) {
|
|
265
|
+
return { files: [] }
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const tauriConfigPath = join(cwd, "tauri/src-tauri/tauri.conf.json")
|
|
269
|
+
const cargoManifestPath = join(cwd, "tauri/src-tauri/Cargo.toml")
|
|
270
|
+
const cargoLockPath = join(cwd, "tauri/src-tauri/Cargo.lock")
|
|
271
|
+
const tauriConfigSource = await readFile(tauriConfigPath, "utf8")
|
|
272
|
+
const cargoManifestSource = await readFile(cargoManifestPath, "utf8")
|
|
273
|
+
const packageName = readCargoPackageName(cargoManifestSource, cargoManifestPath)
|
|
274
|
+
const files = [
|
|
275
|
+
{ path: tauriConfigPath, source: tauriConfigSource },
|
|
276
|
+
{ path: cargoManifestPath, source: cargoManifestSource },
|
|
277
|
+
]
|
|
278
|
+
const nextFiles = []
|
|
279
|
+
|
|
280
|
+
const tauriConfig = JSON.parse(tauriConfigSource) as {
|
|
281
|
+
bundle?: Record<string, unknown>
|
|
282
|
+
version?: string
|
|
283
|
+
[key: string]: unknown
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
tauriConfig.version = version
|
|
287
|
+
tauriConfig.bundle = {
|
|
288
|
+
...(tauriConfig.bundle ?? {}),
|
|
289
|
+
createUpdaterArtifacts: true,
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
nextFiles.push({ path: tauriConfigPath, source: `${JSON.stringify(tauriConfig, null, 2)}\n` })
|
|
293
|
+
nextFiles.push({
|
|
294
|
+
path: cargoManifestPath,
|
|
295
|
+
source: replaceCargoPackageVersion(
|
|
296
|
+
cargoManifestSource,
|
|
297
|
+
packageName,
|
|
298
|
+
version,
|
|
299
|
+
cargoManifestPath,
|
|
300
|
+
),
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
if (await pathExists(cargoLockPath)) {
|
|
304
|
+
const cargoLockSource = await readFile(cargoLockPath, "utf8")
|
|
305
|
+
|
|
306
|
+
files.push({ path: cargoLockPath, source: cargoLockSource })
|
|
307
|
+
nextFiles.push({
|
|
308
|
+
path: cargoLockPath,
|
|
309
|
+
source: replaceCargoLockPackageVersion(cargoLockSource, packageName, version, cargoLockPath),
|
|
310
|
+
})
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
for (const file of nextFiles) {
|
|
314
|
+
await writeFile(file.path, file.source)
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return { files }
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
export async function restoreTauriReleaseMetadata(snapshot: TauriReleaseMetadataSnapshot) {
|
|
321
|
+
for (const file of snapshot.files) {
|
|
322
|
+
await writeFile(file.path, file.source)
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
export async function prepareTauriRelease({
|
|
327
|
+
env,
|
|
328
|
+
plan,
|
|
329
|
+
releaseName,
|
|
330
|
+
version,
|
|
331
|
+
}: {
|
|
332
|
+
env: NodeJS.ProcessEnv
|
|
333
|
+
plan: TauriReleasePlan
|
|
334
|
+
releaseName: string
|
|
335
|
+
version: string
|
|
336
|
+
}): Promise<PreparedTauriRelease | undefined> {
|
|
337
|
+
if (!plan.enabled) {
|
|
338
|
+
return undefined
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
await resetGeneratedDirectory(plan.artifactDownloadDir)
|
|
342
|
+
await resetGeneratedDirectory(plan.releaseUploadDir)
|
|
343
|
+
|
|
344
|
+
const workflowRun =
|
|
345
|
+
plan.workflowPlatforms.length > 0
|
|
346
|
+
? await startTauriWorkflowBuild({ plan, releaseName, version })
|
|
347
|
+
: undefined
|
|
348
|
+
|
|
349
|
+
for (const platform of plan.localPlatforms) {
|
|
350
|
+
await buildLocalTauriPlatform(platform, plan, env)
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
if (workflowRun) {
|
|
354
|
+
const runId = await waitForTauriWorkflowBuild(plan, workflowRun.runNameMarker)
|
|
355
|
+
await nanoSpawn("gh", ["run", "download", String(runId), "--dir", plan.artifactDownloadDir], {
|
|
356
|
+
stdio: "inherit",
|
|
357
|
+
})
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
return createTauriReleaseUpload({ plan, version })
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
export async function uploadTauriReleaseToB2(
|
|
364
|
+
plan: TauriReleasePlan,
|
|
365
|
+
release: PreparedTauriRelease,
|
|
366
|
+
) {
|
|
367
|
+
await nanoSpawn(
|
|
368
|
+
"b2",
|
|
369
|
+
["account", "authorize", plan.b2ApplicationKeyId as string, plan.b2ApplicationKey as string],
|
|
370
|
+
{ stdin: "ignore" },
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
const files = await glob("**/*", { cwd: release.uploadDir, onlyFiles: true })
|
|
374
|
+
const sortedFiles = files.sort((left, right) => {
|
|
375
|
+
if (left === release.manifestPath) {
|
|
376
|
+
return 1
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (right === release.manifestPath) {
|
|
380
|
+
return -1
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
return left.localeCompare(right)
|
|
384
|
+
})
|
|
385
|
+
|
|
386
|
+
for (const file of sortedFiles) {
|
|
387
|
+
await nanoSpawn(
|
|
388
|
+
"b2",
|
|
389
|
+
["file", "upload", plan.b2Bucket as string, join(release.uploadDir, file), file],
|
|
390
|
+
{ stdin: "ignore" },
|
|
391
|
+
)
|
|
392
|
+
console.log(`Uploaded ${file}`)
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
export function createTauriUpdateManifest({
|
|
397
|
+
artifacts,
|
|
398
|
+
notes,
|
|
399
|
+
pubDate = new Date(),
|
|
400
|
+
version,
|
|
401
|
+
}: {
|
|
402
|
+
artifacts: { signature: string; updatePlatform: string; url: string }[]
|
|
403
|
+
notes?: string
|
|
404
|
+
pubDate?: Date
|
|
405
|
+
version: string
|
|
406
|
+
}): TauriUpdateManifest {
|
|
407
|
+
return {
|
|
408
|
+
...(notes ? { notes } : {}),
|
|
409
|
+
platforms: Object.fromEntries(
|
|
410
|
+
artifacts.map((artifact) => [
|
|
411
|
+
artifact.updatePlatform,
|
|
412
|
+
{ signature: artifact.signature, url: artifact.url },
|
|
413
|
+
]),
|
|
414
|
+
),
|
|
415
|
+
pub_date: pubDate.toISOString(),
|
|
416
|
+
version,
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
export async function createTauriReleaseUpload({
|
|
421
|
+
plan,
|
|
422
|
+
version,
|
|
423
|
+
}: {
|
|
424
|
+
plan: TauriReleasePlan
|
|
425
|
+
version: string
|
|
426
|
+
}): Promise<PreparedTauriRelease> {
|
|
427
|
+
const releasePath = joinRemotePath(
|
|
428
|
+
plan.publicPathPrefix,
|
|
429
|
+
formatReleasePath(plan.releasePath, version),
|
|
430
|
+
)
|
|
431
|
+
const latestManifestPath = joinRemotePath(plan.publicPathPrefix, plan.latestManifestPath)
|
|
432
|
+
const selectedPlatforms = [...plan.localPlatforms, ...plan.workflowPlatforms]
|
|
433
|
+
const manifestArtifacts = []
|
|
434
|
+
|
|
435
|
+
for (const platform of selectedPlatforms) {
|
|
436
|
+
const platformConfig = plan.platform[platform]
|
|
437
|
+
const sourceRoot = plan.localPlatforms.includes(platform)
|
|
438
|
+
? "."
|
|
439
|
+
: join(plan.artifactDownloadDir, `tauri-${platform}-${version}`)
|
|
440
|
+
const artifactPair = await findTauriUpdateArtifact({
|
|
441
|
+
globs: plan.localPlatforms.includes(platform) ? platformConfig.artifactGlobs : ["**/*.sig"],
|
|
442
|
+
sourceRoot,
|
|
443
|
+
version,
|
|
444
|
+
})
|
|
445
|
+
const remoteDir = joinRemotePath(releasePath, platformConfig.updatePlatform)
|
|
446
|
+
const artifactRemotePath = joinRemotePath(remoteDir, basename(artifactPair.artifactPath))
|
|
447
|
+
const signatureRemotePath = joinRemotePath(remoteDir, basename(artifactPair.signaturePath))
|
|
448
|
+
const uploadArtifactPath = join(plan.releaseUploadDir, ...artifactRemotePath.split("/"))
|
|
449
|
+
const uploadSignaturePath = join(plan.releaseUploadDir, ...signatureRemotePath.split("/"))
|
|
450
|
+
const signature = (await readFile(artifactPair.signaturePath, "utf8")).trim()
|
|
451
|
+
|
|
452
|
+
await mkdir(dirname(uploadArtifactPath), { recursive: true })
|
|
453
|
+
await cp(artifactPair.artifactPath, uploadArtifactPath)
|
|
454
|
+
await cp(artifactPair.signaturePath, uploadSignaturePath)
|
|
455
|
+
manifestArtifacts.push({
|
|
456
|
+
signature,
|
|
457
|
+
updatePlatform: platformConfig.updatePlatform,
|
|
458
|
+
url: joinPublicUrl(plan.publicBaseUrl as string, artifactRemotePath),
|
|
459
|
+
})
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
const notes = plan.releaseNotesPath ? await readFile(plan.releaseNotesPath, "utf8") : undefined
|
|
463
|
+
const manifest = createTauriUpdateManifest({ artifacts: manifestArtifacts, notes, version })
|
|
464
|
+
const manifestPath = join(plan.releaseUploadDir, ...latestManifestPath.split("/"))
|
|
465
|
+
|
|
466
|
+
await mkdir(dirname(manifestPath), { recursive: true })
|
|
467
|
+
await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`)
|
|
468
|
+
|
|
469
|
+
return {
|
|
470
|
+
manifestPath: latestManifestPath,
|
|
471
|
+
uploadDir: plan.releaseUploadDir,
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
async function startTauriWorkflowBuild({
|
|
476
|
+
plan,
|
|
477
|
+
releaseName,
|
|
478
|
+
version,
|
|
479
|
+
}: {
|
|
480
|
+
plan: TauriReleasePlan
|
|
481
|
+
releaseName: string
|
|
482
|
+
version: string
|
|
483
|
+
}) {
|
|
484
|
+
const ref = plan.workflowRef ?? (await currentGitRef())
|
|
485
|
+
const runNameMarker = `coreframe-${Date.now()}-${randomUUID().slice(0, 8)}`
|
|
486
|
+
|
|
487
|
+
await nanoSpawn(
|
|
488
|
+
"gh",
|
|
489
|
+
[
|
|
490
|
+
"workflow",
|
|
491
|
+
"run",
|
|
492
|
+
plan.workflowName,
|
|
493
|
+
"--ref",
|
|
494
|
+
ref,
|
|
495
|
+
"-f",
|
|
496
|
+
`release_name=${releaseName}`,
|
|
497
|
+
"-f",
|
|
498
|
+
`run_id=${runNameMarker}`,
|
|
499
|
+
"-f",
|
|
500
|
+
`version=${version}`,
|
|
501
|
+
...tauriReleasePlatforms.flatMap((platform) => [
|
|
502
|
+
"-f",
|
|
503
|
+
`${platform}=${plan.workflowPlatforms.includes(platform) ? "true" : "false"}`,
|
|
504
|
+
]),
|
|
505
|
+
],
|
|
506
|
+
{ stdin: "ignore" },
|
|
507
|
+
)
|
|
508
|
+
|
|
509
|
+
return { runNameMarker }
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
async function waitForTauriWorkflowBuild(plan: TauriReleasePlan, runNameMarker: string) {
|
|
513
|
+
const deadline = Date.now() + plan.workflowTimeoutMs
|
|
514
|
+
|
|
515
|
+
while (Date.now() < deadline) {
|
|
516
|
+
const run = await findTauriWorkflowRun(plan, runNameMarker)
|
|
517
|
+
|
|
518
|
+
if (run?.status === "completed") {
|
|
519
|
+
if (run.conclusion === "success") {
|
|
520
|
+
return run.databaseId
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
throw new Error(`Tauri release workflow ${run.databaseId} finished with ${run.conclusion}.`)
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
await delay(plan.workflowPollIntervalMs)
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
throw new Error(
|
|
530
|
+
`Timed out waiting for Tauri release workflow after ${Math.round(plan.workflowTimeoutMs / 60_000)} minutes.`,
|
|
531
|
+
)
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
async function findTauriWorkflowRun(plan: TauriReleasePlan, runNameMarker: string) {
|
|
535
|
+
const { stdout } = await nanoSpawn(
|
|
536
|
+
"gh",
|
|
537
|
+
[
|
|
538
|
+
"run",
|
|
539
|
+
"list",
|
|
540
|
+
"--workflow",
|
|
541
|
+
plan.workflowName,
|
|
542
|
+
"--event",
|
|
543
|
+
"workflow_dispatch",
|
|
544
|
+
"--json",
|
|
545
|
+
"conclusion,databaseId,displayTitle,status",
|
|
546
|
+
"--limit",
|
|
547
|
+
"50",
|
|
548
|
+
],
|
|
549
|
+
{ stdin: "ignore" },
|
|
550
|
+
)
|
|
551
|
+
const runs = JSON.parse(stdout) as {
|
|
552
|
+
conclusion: string | null
|
|
553
|
+
databaseId: number
|
|
554
|
+
displayTitle: string
|
|
555
|
+
status: string
|
|
556
|
+
}[]
|
|
557
|
+
|
|
558
|
+
return runs.find((run) => run.displayTitle.includes(runNameMarker))
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
async function buildLocalTauriPlatform(
|
|
562
|
+
platform: TauriReleasePlatform,
|
|
563
|
+
plan: TauriReleasePlan,
|
|
564
|
+
env: NodeJS.ProcessEnv,
|
|
565
|
+
) {
|
|
566
|
+
await nanoSpawn("pnpm", plan.platform[platform].buildArgs, {
|
|
567
|
+
env,
|
|
568
|
+
stdio: "inherit",
|
|
569
|
+
})
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
async function findTauriUpdateArtifact({
|
|
573
|
+
globs,
|
|
574
|
+
sourceRoot,
|
|
575
|
+
version,
|
|
576
|
+
}: {
|
|
577
|
+
globs: string[]
|
|
578
|
+
sourceRoot: string
|
|
579
|
+
version: string
|
|
580
|
+
}) {
|
|
581
|
+
const signaturePaths = await glob(globs, { cwd: sourceRoot, absolute: true, onlyFiles: true })
|
|
582
|
+
const candidatePairs = []
|
|
583
|
+
|
|
584
|
+
for (const signaturePath of signaturePaths) {
|
|
585
|
+
const artifactPath = signaturePath.replace(/\.sig$/u, "")
|
|
586
|
+
|
|
587
|
+
if (await isFile(artifactPath)) {
|
|
588
|
+
candidatePairs.push({ artifactPath, signaturePath })
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
if (candidatePairs.length === 0) {
|
|
593
|
+
throw new Error(`Could not find a Tauri updater artifact and .sig pair under ${sourceRoot}.`)
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
return candidatePairs.sort((left, right) => {
|
|
597
|
+
const leftScore = artifactScore(left.artifactPath, version)
|
|
598
|
+
const rightScore = artifactScore(right.artifactPath, version)
|
|
599
|
+
|
|
600
|
+
return rightScore - leftScore || left.artifactPath.localeCompare(right.artifactPath)
|
|
601
|
+
})[0]
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
function artifactScore(path: string, version: string) {
|
|
605
|
+
let score = 0
|
|
606
|
+
|
|
607
|
+
if (path.includes(version)) {
|
|
608
|
+
score += 100
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
if (/\.(app\.tar\.gz|AppImage\.tar\.gz|zip)$/u.test(path)) {
|
|
612
|
+
score += 10
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
if (/\.(exe|msi|dmg|AppImage|deb|rpm|apk|aab|ipa)$/u.test(path)) {
|
|
616
|
+
score += 5
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
return score
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
function resolvePlatformConfig(
|
|
623
|
+
config: TauriReleaseConfig["platform"] = {},
|
|
624
|
+
env: NodeJS.ProcessEnv,
|
|
625
|
+
): Record<TauriReleasePlatform, ResolvedTauriReleasePlatformConfig> {
|
|
626
|
+
return Object.fromEntries(
|
|
627
|
+
tauriReleasePlatforms.map((platform) => {
|
|
628
|
+
const override = config[platform] ?? {}
|
|
629
|
+
const platformConfig = {
|
|
630
|
+
...defaultPlatformConfig[platform],
|
|
631
|
+
...override,
|
|
632
|
+
}
|
|
633
|
+
const envTargetTriple = env[`TAURI_RELEASE_${platform.toUpperCase()}_TARGET_TRIPLE`]
|
|
634
|
+
const envUpdatePlatform = env[`TAURI_RELEASE_${platform.toUpperCase()}_UPDATE_PLATFORM`]
|
|
635
|
+
const targetTriple = envTargetTriple ?? platformConfig.targetTriple
|
|
636
|
+
const buildArgs = override.buildArgs
|
|
637
|
+
? override.buildArgs
|
|
638
|
+
: defaultBuildArgs(platform, targetTriple)
|
|
639
|
+
|
|
640
|
+
return [
|
|
641
|
+
platform,
|
|
642
|
+
{
|
|
643
|
+
...platformConfig,
|
|
644
|
+
artifactGlobs:
|
|
645
|
+
platformConfig.artifactGlobs ?? defaultPlatformConfig[platform].artifactGlobs,
|
|
646
|
+
buildArgs,
|
|
647
|
+
targetTriple,
|
|
648
|
+
updatePlatform: envUpdatePlatform ?? platformConfig.updatePlatform,
|
|
649
|
+
},
|
|
650
|
+
]
|
|
651
|
+
}),
|
|
652
|
+
) as Record<TauriReleasePlatform, ResolvedTauriReleasePlatformConfig>
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
function defaultBuildArgs(platform: TauriReleasePlatform, targetTriple: string) {
|
|
656
|
+
if (platform === "android") {
|
|
657
|
+
return ["tauri", "android", "build", "--target", targetTriple]
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
if (platform === "ios") {
|
|
661
|
+
return ["tauri", "ios", "build", "--target", targetTriple]
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
return ["tauri", "build", "--target", targetTriple, "--", "--locked"]
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
function readCargoPackageName(source: string, path: string) {
|
|
668
|
+
const match = /^\[package\][\s\S]*?^name = "(?<name>[^"]+)"/mu.exec(source)
|
|
669
|
+
|
|
670
|
+
if (!match?.groups?.name) {
|
|
671
|
+
throw new Error(`${path} is missing a [package] name.`)
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
return match.groups.name
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
function replaceCargoPackageVersion(
|
|
678
|
+
source: string,
|
|
679
|
+
packageName: string,
|
|
680
|
+
version: string,
|
|
681
|
+
path: string,
|
|
682
|
+
) {
|
|
683
|
+
const pattern = new RegExp(
|
|
684
|
+
`(^\\[package\\][\\s\\S]*?^name = "${escapeRegExp(packageName)}"[\\s\\S]*?^version = ")[^"]+(")`,
|
|
685
|
+
"mu",
|
|
686
|
+
)
|
|
687
|
+
const nextSource = source.replace(pattern, `$1${version}$2`)
|
|
688
|
+
|
|
689
|
+
if (nextSource === source) {
|
|
690
|
+
throw new Error(`${path} is missing a package version for ${packageName}.`)
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
return nextSource
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
function replaceCargoLockPackageVersion(
|
|
697
|
+
source: string,
|
|
698
|
+
packageName: string,
|
|
699
|
+
version: string,
|
|
700
|
+
path: string,
|
|
701
|
+
) {
|
|
702
|
+
const pattern = new RegExp(
|
|
703
|
+
`(\\[\\[package\\]\\]\\nname = "${escapeRegExp(packageName)}"\\nversion = ")[^"]+(")`,
|
|
704
|
+
"u",
|
|
705
|
+
)
|
|
706
|
+
const nextSource = source.replace(pattern, `$1${version}$2`)
|
|
707
|
+
|
|
708
|
+
if (nextSource === source) {
|
|
709
|
+
throw new Error(`${path} is missing a package lock entry for ${packageName}.`)
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
return nextSource
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
async function currentGitRef() {
|
|
716
|
+
const { stdout: branch } = await nanoSpawn("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
|
|
717
|
+
stdin: "ignore",
|
|
718
|
+
})
|
|
719
|
+
const trimmedBranch = branch.trim()
|
|
720
|
+
|
|
721
|
+
if (trimmedBranch !== "HEAD") {
|
|
722
|
+
return trimmedBranch
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
const { stdout: commit } = await nanoSpawn("git", ["rev-parse", "HEAD"], {
|
|
726
|
+
stdin: "ignore",
|
|
727
|
+
})
|
|
728
|
+
|
|
729
|
+
return commit.trim()
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
async function resetGeneratedDirectory(path: string) {
|
|
733
|
+
assertSafeGeneratedDirectory(path)
|
|
734
|
+
await rm(path, { force: true, recursive: true })
|
|
735
|
+
await mkdir(path, { recursive: true })
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
function assertSafeGeneratedDirectory(path: string) {
|
|
739
|
+
const resolvedPath = resolve(path)
|
|
740
|
+
const relativePath = relative(process.cwd(), resolvedPath).split(sep).join("/")
|
|
741
|
+
|
|
742
|
+
if (
|
|
743
|
+
relativePath === "" ||
|
|
744
|
+
relativePath === "." ||
|
|
745
|
+
relativePath.startsWith("../") ||
|
|
746
|
+
relativePath === "tauri" ||
|
|
747
|
+
relativePath.startsWith("tauri/")
|
|
748
|
+
) {
|
|
749
|
+
throw new Error(`Refusing to reset unsafe Tauri release directory: ${path}`)
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
function formatReleasePath(template: string, version: string) {
|
|
754
|
+
return template.replaceAll("${version}", version)
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
function joinPublicUrl(baseUrl: string, remotePath: string) {
|
|
758
|
+
const url = new URL(baseUrl)
|
|
759
|
+
const basePath = url.pathname.replace(/\/+$/u, "")
|
|
760
|
+
const nextPath = joinRemotePath(basePath, remotePath)
|
|
761
|
+
|
|
762
|
+
url.pathname = nextPath.startsWith("/") ? nextPath : `/${nextPath}`
|
|
763
|
+
|
|
764
|
+
return url.toString()
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
function joinRemotePath(...parts: string[]) {
|
|
768
|
+
return parts
|
|
769
|
+
.flatMap((part) => part.split("/"))
|
|
770
|
+
.map((part) => part.trim())
|
|
771
|
+
.filter(Boolean)
|
|
772
|
+
.join("/")
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
function parsePositiveInteger(value: string | undefined, name: string) {
|
|
776
|
+
if (!value) {
|
|
777
|
+
return 0
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
const parsed = Number(value)
|
|
781
|
+
|
|
782
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
783
|
+
throw new Error(`${name} must be a positive integer.`)
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
return parsed
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
function uniquePlatforms(platforms: TauriReleasePlatform[]) {
|
|
790
|
+
return [...new Set(platforms)]
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
function isTauriReleasePlatform(value: string): value is TauriReleasePlatform {
|
|
794
|
+
return (tauriReleasePlatforms as readonly string[]).includes(value)
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
async function pathExists(path: string) {
|
|
798
|
+
try {
|
|
799
|
+
await access(path, constants.F_OK)
|
|
800
|
+
return true
|
|
801
|
+
} catch {
|
|
802
|
+
return false
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
async function isFile(path: string) {
|
|
807
|
+
try {
|
|
808
|
+
return (await stat(path)).isFile()
|
|
809
|
+
} catch {
|
|
810
|
+
return false
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
function escapeRegExp(value: string) {
|
|
815
|
+
return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&")
|
|
816
|
+
}
|