Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions packages/edge-bundler/node/bridge.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,3 +136,48 @@ test('Does inherit environment variables if `extendEnv` is not set', async () =>

await rm(tmpDir.path, { force: true, recursive: true, maxRetries: 10 })
})

test('Provides actionable error message when downloaded binary cannot be executed', async () => {
const tmpDir = await tmp.dir()
const latestVersion = semver.minVersion(DENO_VERSION_RANGE)?.version ?? ''
const data = new PassThrough()
const archive = archiver('zip', { zlib: { level: 9 } })

archive.pipe(data)
// Create a binary that will fail to execute (invalid content)
archive.append(Buffer.from('invalid binary content'), {
name: platform === 'win32' ? 'deno.exe' : 'deno',
})
archive.finalize()

const target = getPlatformTarget()

nock('https://dl.deno.land').get('/release-latest.txt').reply(200, `v${latestVersion}`)
nock('https://dl.deno.land')
.get(`/release/v${latestVersion}/deno-${target}.zip`)
.reply(200, () => data)

const deno = new DenoBridge({
cacheDirectory: tmpDir.path,
useGlobal: false,
})

try {
await deno.getBinaryPath()
expect.fail('Should have thrown an error')
} catch (error) {
expect(error).toBeInstanceOf(Error)
const errorMessage = (error as Error).message

expect(errorMessage).toContain('Failed to set up Deno for Edge Functions')
expect(errorMessage).toMatch(/Error:/)
expect(errorMessage).toMatch(/Downloaded to: .+deno(\.exe)?/)
expect(errorMessage).toContain(tmpDir.path)
expect(errorMessage).toMatch(/Platform: (darwin|linux|win32)\/(x64|arm64|ia32)/)
expect(errorMessage).toContain('This may be caused by permissions, antivirus software, or platform incompatibility')
expect(errorMessage).toContain('Try clearing the Deno cache directory and retrying')
expect(errorMessage).toContain('https://ntl.fyi/install-deno')
}

await rm(tmpDir.path, { force: true, recursive: true, maxRetries: 10 })
})
39 changes: 26 additions & 13 deletions packages/edge-bundler/node/bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,14 +84,24 @@ export class DenoBridge {
this.logger.system(`Downloading Deno CLI to ${this.cacheDirectory}`)

const binaryPath = await download(this.cacheDirectory, this.versionRange, this.logger)
const downloadedVersion = await this.getBinaryVersion(binaryPath)
const result = await this.getBinaryVersion(binaryPath)

// We should never get here, because it means that `DENO_VERSION_RANGE` is
// a malformed semver range. If this does happen, let's throw an error so
// that the tests catch it.
if (downloadedVersion === undefined) {
// If we can't execute the downloaded binary, provide actionable info to diagnose and self-heal if possible
if (result.error) {
const error = new Error(
'There was a problem setting up the Edge Functions environment. To try a manual installation, visit https://ntl.fyi/install-deno.',
`Failed to set up Deno for Edge Functions.
Error: ${result.error.message}
Downloaded to: ${binaryPath}
Platform: ${process.platform}/${process.arch}

This may be caused by permissions, antivirus software, or platform incompatibility.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this line could be better 🤔


Try clearing the Deno cache directory and retrying:
${this.cacheDirectory}
Comment on lines +99 to +100
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I decided not to bother dynamically producing a platform-specific command here. I think users can figure out how to delete a directory.


Supported Deno versions: ${this.versionRange}

To install Deno manually: https://ntl.fyi/install-deno`,
)

await this.onAfterDownload?.(error)
Expand All @@ -101,26 +111,29 @@ export class DenoBridge {
throw error
}

await this.writeVersionFile(downloadedVersion)
await this.writeVersionFile(result.version)

await this.onAfterDownload?.()

return binaryPath
}

async getBinaryVersion(binaryPath: string) {
async getBinaryVersion(
binaryPath: string,
): Promise<{ version: string; error?: undefined } | { version?: undefined; error: Error }> {
try {
const { stdout } = await execa(binaryPath, ['--version'])
const version = stdout.match(/^deno ([\d.]+)/)

if (!version) {
this.logger.system(`getBinaryVersion no version found. binaryPath ${binaryPath}`)
return
return { error: new Error('Could not parse Deno version from output') }
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we include the full stdout here...?

}

return version[1]
return { version: version[1] }
} catch (error) {
this.logger.system('getBinaryVersion failed', error)
return { error: error instanceof Error ? error : new Error(String(error)) }
}
}

Expand Down Expand Up @@ -152,11 +165,11 @@ export class DenoBridge {
}

const globalBinaryName = 'deno'
const globalVersion = await this.getBinaryVersion(globalBinaryName)
const result = await this.getBinaryVersion(globalBinaryName)

if (globalVersion === undefined || !semver.satisfies(globalVersion, this.versionRange)) {
if (result.error || !semver.satisfies(result.version, this.versionRange)) {
this.logger.system(
`No globalVersion or semver not satisfied. globalVersion: ${globalVersion}, versionRange: ${this.versionRange}`,
`No globalVersion or semver not satisfied. globalVersion: ${result.version}, versionRange: ${this.versionRange}`,
)
return
}
Expand Down
3 changes: 2 additions & 1 deletion packages/edge-bundler/node/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,8 @@ export const getFunctionConfig = async ({
const collector = await tmp.file()

// Retrieving the version of Deno.
const version = new SemVer((await deno.getBinaryVersion((await deno.getBinaryPath({ silent: true })).path)) || '')
const result = await deno.getBinaryVersion((await deno.getBinaryPath({ silent: true })).path)
const version = new SemVer(result.version || '')

// The extractor will use its exit code to signal different error scenarios,
// based on the list of exit codes we send as an argument. We then capture
Expand Down
Loading