Skip to content

Commit 0c166f6

Browse files
fix: offer backwards compatibility for Deno 1.x features (#6751)
1 parent 573c70d commit 0c166f6

File tree

14 files changed

+128
-105
lines changed

14 files changed

+128
-105
lines changed

packages/edge-bundler/deno/extract.ts

Lines changed: 0 additions & 7 deletions
This file was deleted.

packages/edge-bundler/node/bridge.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,14 @@ import semver from 'semver'
99
import tmp, { DirectoryResult } from 'tmp-promise'
1010
import { test, expect } from 'vitest'
1111

12-
import { DenoBridge, DENO_VERSION_RANGE } from './bridge.js'
12+
import { DenoBridge, LEGACY_DENO_VERSION_RANGE } from './bridge.js'
1313
import { getPlatformTarget } from './platform.js'
1414

1515
const require = createRequire(import.meta.url)
1616
const archiver = require('archiver')
1717

1818
const getMockDenoBridge = function (tmpDir: DirectoryResult, mockBinaryOutput: string) {
19-
const latestVersion = semver.minVersion(DENO_VERSION_RANGE)?.version ?? ''
19+
const latestVersion = semver.minVersion(LEGACY_DENO_VERSION_RANGE)?.version ?? ''
2020
const data = new PassThrough()
2121
const archive = archiver('zip', { zlib: { level: 9 } })
2222

@@ -139,7 +139,7 @@ test('Does inherit environment variables if `extendEnv` is not set', async () =>
139139

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

packages/edge-bundler/node/bridge.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,12 @@ import { getBinaryExtension } from './platform.js'
1414

1515
const DENO_VERSION_FILE = 'version.txt'
1616

17+
export const LEGACY_DENO_VERSION_RANGE = '1.39.0 - 2.2.4'
18+
1719
// When updating DENO_VERSION_RANGE, ensure that the deno version
1820
// on the netlify/buildbot build image satisfies this range!
1921
// https://github.com/netlify/buildbot/blob/f9c03c9dcb091d6570e9d0778381560d469e78ad/build-image/noble/Dockerfile#L410
20-
export const DENO_VERSION_RANGE = '1.39.0 - 2.2.4'
21-
22-
const NEXT_DENO_VERSION_RANGE = '^2.4.2'
22+
const DENO_VERSION_RANGE = '^2.4.2'
2323

2424
export type OnBeforeDownloadHook = () => void | Promise<void>
2525
export type OnAfterDownloadHook = (error?: Error) => void | Promise<void>
@@ -75,7 +75,7 @@ export class DenoBridge {
7575
options.featureFlags?.edge_bundler_generate_tarball ||
7676
options.featureFlags?.edge_bundler_deno_v2
7777

78-
this.versionRange = options.versionRange ?? (useNextDeno ? NEXT_DENO_VERSION_RANGE : DENO_VERSION_RANGE)
78+
this.versionRange = options.versionRange ?? (useNextDeno ? DENO_VERSION_RANGE : LEGACY_DENO_VERSION_RANGE)
7979
}
8080

8181
private async downloadBinary() {

packages/edge-bundler/node/bundler.test.ts

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -626,8 +626,8 @@ test('Loads JSON modules with `with` attribute', async () => {
626626
await rm(vendorDirectory.path, { force: true, recursive: true })
627627
})
628628

629-
test('Emits a system log when import assertions are used', async () => {
630-
const { basePath, cleanup, distPath } = await useFixture('with_import_assert')
629+
test('Is backwards compatible with Deno 1.x', async () => {
630+
const { basePath, cleanup, distPath } = await useFixture('with_deno_1x_features')
631631
const sourceDirectory = join(basePath, 'functions')
632632
const vendorDirectory = await tmp.dir()
633633
const systemLogger = vi.fn()
@@ -643,18 +643,45 @@ test('Emits a system log when import assertions are used', async () => {
643643

644644
const manifestFile = await readFile(resolve(distPath, 'manifest.json'), 'utf8')
645645
const manifest = JSON.parse(manifestFile)
646-
const bundlePath = join(distPath, manifest.bundles[0].asset)
647-
const { func1 } = await runESZIP(bundlePath, vendorDirectory.path)
648646

649-
expect(func1).toBe(`{"foo":"bar"}`)
650647
expect(systemLogger).toHaveBeenCalledWith(
651648
`Edge function uses import assertions: ${join(sourceDirectory, 'func1.ts')}`,
652649
)
653650
expect(manifest.routes[0]).toEqual({
654651
function: 'func1',
655-
pattern: '^/with-import-assert/?$',
652+
pattern: '^/with-import-assert-ts/?$',
653+
excluded_patterns: [],
654+
path: '/with-import-assert-ts',
655+
})
656+
657+
expect(systemLogger).toHaveBeenCalledWith(
658+
`Edge function uses import assertions: ${join(sourceDirectory, 'func2.js')}`,
659+
)
660+
expect(manifest.routes[1]).toEqual({
661+
function: 'func2',
662+
pattern: '^/with-import-assert-js/?$',
663+
excluded_patterns: [],
664+
path: '/with-import-assert-js',
665+
})
666+
667+
expect(systemLogger).toHaveBeenCalledWith(
668+
`Edge function uses the window global: ${join(sourceDirectory, 'func3.ts')}`,
669+
)
670+
expect(manifest.routes[2]).toEqual({
671+
function: 'func3',
672+
pattern: '^/with-window-global-ts/?$',
673+
excluded_patterns: [],
674+
path: '/with-window-global-ts',
675+
})
676+
677+
expect(systemLogger).toHaveBeenCalledWith(
678+
`Edge function uses the window global: ${join(sourceDirectory, 'func4.js')}`,
679+
)
680+
expect(manifest.routes[3]).toEqual({
681+
function: 'func4',
682+
pattern: '^/with-window-global-js/?$',
656683
excluded_patterns: [],
657-
path: '/with-import-assert',
684+
path: '/with-window-global-js',
658685
})
659686

660687
await cleanup()

packages/edge-bundler/node/bundler.ts

Lines changed: 51 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,34 @@
11
import { promises as fs } from 'fs'
2-
import { join, relative } from 'path'
2+
import { join } from 'path'
33

44
import commonPathPrefix from 'common-path-prefix'
55
import { v4 as uuidv4 } from 'uuid'
66

77
import { importMapSpecifier } from '../shared/consts.js'
88

9-
import { DenoBridge, DenoOptions, OnAfterDownloadHook, OnBeforeDownloadHook } from './bridge.js'
9+
import {
10+
DenoBridge,
11+
DenoOptions,
12+
OnAfterDownloadHook,
13+
OnBeforeDownloadHook,
14+
LEGACY_DENO_VERSION_RANGE,
15+
} from './bridge.js'
1016
import type { Bundle } from './bundle.js'
1117
import { FunctionConfig, getFunctionConfig } from './config.js'
1218
import { Declaration, mergeDeclarations } from './declaration.js'
1319
import { load as loadDeployConfig } from './deploy_config.js'
1420
import { EdgeFunction } from './edge_function.js'
1521
import { FeatureFlags, getFlags } from './feature_flags.js'
1622
import { findFunctions } from './finder.js'
17-
import { bundle as bundleESZIP, extension as eszipExtension, extract as extractESZIP } from './formats/eszip.js'
23+
import { bundle as bundleESZIP } from './formats/eszip.js'
1824
import { bundle as bundleTarball } from './formats/tarball.js'
1925
import { ImportMap } from './import_map.js'
2026
import { getLogger, LogFunction, Logger } from './logger.js'
2127
import { writeManifest } from './manifest.js'
2228
import { vendorNPMSpecifiers } from './npm_dependencies.js'
2329
import { ensureLatestTypes } from './types.js'
2430
import { nonNullable } from './utils/non_nullable.js'
25-
import { BundleError } from './bundle_error.js'
31+
import { getPathInHome } from './home_path.js'
2632

2733
export interface BundleOptions {
2834
basePath?: string
@@ -172,15 +178,11 @@ export const bundle = async (
172178
// The final file name of the bundles contains a SHA256 hash of the contents,
173179
// which we can only compute now that the files have been generated. So let's
174180
// rename the bundles to their permanent names.
175-
const bundlePaths = await createFinalBundles(bundles, distDirectory, buildID)
176-
const eszipPath = bundlePaths.find((path) => path.endsWith(eszipExtension))
181+
await createFinalBundles(bundles, distDirectory, buildID)
177182

178183
const { internalFunctions: internalFunctionsWithConfig, userFunctions: userFunctionsWithConfig } =
179184
await getFunctionConfigs({
180-
basePath,
181185
deno,
182-
eszipPath,
183-
featureFlags,
184186
importMap,
185187
internalFunctions,
186188
log: logger,
@@ -224,81 +226,65 @@ export const bundle = async (
224226
}
225227

226228
interface GetFunctionConfigsOptions {
227-
basePath: string
228229
deno: DenoBridge
229-
eszipPath?: string
230-
featureFlags?: FeatureFlags
231230
importMap: ImportMap
232231
internalFunctions: EdgeFunction[]
233232
log: Logger
234233
userFunctions: EdgeFunction[]
235234
}
236235

237236
const getFunctionConfigs = async ({
238-
basePath,
239237
deno,
240-
eszipPath,
241-
featureFlags,
242238
importMap,
243239
log,
244240
internalFunctions,
245241
userFunctions,
246242
}: GetFunctionConfigsOptions) => {
247-
try {
248-
const internalConfigPromises = internalFunctions.map(
249-
async (func) => [func.name, await getFunctionConfig({ functionPath: func.path, importMap, deno, log })] as const,
250-
)
251-
const userConfigPromises = userFunctions.map(
252-
async (func) => [func.name, await getFunctionConfig({ functionPath: func.path, importMap, deno, log })] as const,
253-
)
254-
255-
// Creating a hash of function names to configuration objects.
256-
const internalFunctionsWithConfig = Object.fromEntries(await Promise.all(internalConfigPromises))
257-
const userFunctionsWithConfig = Object.fromEntries(await Promise.all(userConfigPromises))
258-
259-
return {
260-
internalFunctions: internalFunctionsWithConfig,
261-
userFunctions: userFunctionsWithConfig,
262-
}
263-
} catch (err) {
264-
if (!(err instanceof Error && err.cause === 'IMPORT_ASSERT') || !eszipPath || !featureFlags?.edge_bundler_deno_v2) {
265-
throw err
266-
}
243+
const functions = [...internalFunctions, ...userFunctions]
244+
const results = await Promise.allSettled(
245+
functions.map(async (func) => {
246+
return [func.name, await getFunctionConfig({ functionPath: func.path, importMap, deno, log })] as const
247+
}),
248+
)
249+
const legacyDeno = new DenoBridge({
250+
cacheDirectory: getPathInHome('deno-cli-v1'),
251+
useGlobal: false,
252+
versionRange: LEGACY_DENO_VERSION_RANGE,
253+
})
267254

268-
log.user(
269-
'WARNING: Import assertions are deprecated and will be removed soon. Refer to https://ntl.fyi/import-assert for more information.',
270-
)
255+
for (let i = 0; i < results.length; i++) {
256+
const result = results[i]
257+
const func = functions[i]
258+
259+
// We offer support for some features of Deno 1.x that have been removed
260+
// from 2.x, such as import assertions and the `window` global. When we
261+
// see that we failed to extract a config due to those edge cases, re-run
262+
// the script with Deno 1.x so we can extract the config.
263+
if (
264+
result.status === 'rejected' &&
265+
result.reason instanceof Error &&
266+
(result.reason.cause === 'IMPORT_ASSERT' || result.reason.cause === 'WINDOW_GLOBAL')
267+
) {
268+
try {
269+
const fallbackConfig = await getFunctionConfig({ functionPath: func.path, importMap, deno: legacyDeno, log })
271270

272-
try {
273-
// We failed to extract the configuration because there is an import assert
274-
// in the function code, a deprecated feature that we used to support with
275-
// Deno 1.x. To avoid a breaking change, we treat this error as a special
276-
// case, using the generated ESZIP to extract the configuration. This works
277-
// because import asserts are transpiled to import attributes.
278-
const extractedESZIP = await extractESZIP(deno, eszipPath)
279-
const configs = await Promise.all(
280-
[...internalFunctions, ...userFunctions].map(async (func) => {
281-
const relativePath = relative(basePath, func.path)
282-
const functionPath = join(extractedESZIP.path, relativePath)
271+
results[i] = { status: 'fulfilled', value: [func.name, fallbackConfig] }
272+
} catch {
273+
throw result.reason
274+
}
275+
}
276+
}
283277

284-
return [func.name, await getFunctionConfig({ functionPath, importMap, deno, log })] as const
285-
}),
286-
)
278+
const failure = results.find((result) => result.status === 'rejected')
279+
if (failure) {
280+
throw failure.reason
281+
}
287282

288-
await extractedESZIP.cleanup()
283+
const configs = results.map((config) => (config as PromiseFulfilledResult<[string, FunctionConfig]>).value)
289284

290-
return {
291-
internalFunctions: Object.fromEntries(configs.slice(0, internalFunctions.length)),
292-
userFunctions: Object.fromEntries(configs.slice(internalFunctions.length)),
293-
}
294-
} catch (err) {
295-
throw new BundleError(
296-
new Error(
297-
'An error occurred while building an edge function that uses an import assertion. Refer to https://ntl.fyi/import-assert for more information.',
298-
),
299-
{ cause: err },
300-
)
301-
}
285+
return {
286+
internalFunctions: Object.fromEntries(configs.slice(0, internalFunctions.length)),
287+
userFunctions: Object.fromEntries(configs.slice(internalFunctions.length)),
302288
}
303289
}
304290

packages/edge-bundler/node/config.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,12 +157,21 @@ export const getFunctionConfig = async ({
157157
const handleConfigError = (functionPath: string, exitCode: number, stderr: string, log: Logger) => {
158158
let cause: string | Error | undefined
159159

160-
if (stderr.includes('Import assertions are deprecated')) {
160+
if (
161+
stderr.includes('Import assertions are deprecated') ||
162+
stderr.includes(`SyntaxError: Unexpected identifier 'assert'`)
163+
) {
161164
log.system(`Edge function uses import assertions: ${functionPath}`)
162165

163166
cause = 'IMPORT_ASSERT'
164167
}
165168

169+
if (stderr.includes('ReferenceError: window is not defined')) {
170+
log.system(`Edge function uses the window global: ${functionPath}`)
171+
172+
cause = 'WINDOW_GLOBAL'
173+
}
174+
166175
switch (exitCode) {
167176
case ConfigExitCode.ImportError:
168177
log.user(stderr)

packages/edge-bundler/node/formats/eszip.ts

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
import { join } from 'path'
22
import { pathToFileURL } from 'url'
33

4-
import tmp from 'tmp-promise'
5-
64
import { virtualRoot, virtualVendorRoot } from '../../shared/consts.js'
75
import type { WriteStage2Options } from '../../shared/stage2.js'
86
import { DenoBridge } from '../bridge.js'
@@ -88,16 +86,3 @@ const getESZIPPaths = () => {
8886
importMap: join(denoPath, 'vendor', 'import_map.json'),
8987
}
9088
}
91-
92-
export const extract = async (deno: DenoBridge, functionPath: string) => {
93-
const tmpDir = await tmp.dir({ unsafeCleanup: true })
94-
const { extractor, importMap } = getESZIPPaths()
95-
const flags = ['--allow-all', '--no-config', '--no-lock', `--import-map=${importMap}`, '--quiet']
96-
97-
await deno.run(['run', ...flags, extractor, functionPath, tmpDir.path], { pipeOutput: true })
98-
99-
return {
100-
cleanup: tmpDir.cleanup,
101-
path: join(tmpDir.path, 'source', 'root'),
102-
}
103-
}

packages/edge-bundler/node/main.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,14 @@ import semver from 'semver'
99
import tmp from 'tmp-promise'
1010
import { test, expect, vi } from 'vitest'
1111

12-
import { DenoBridge, DENO_VERSION_RANGE } from './bridge.js'
12+
import { DenoBridge, LEGACY_DENO_VERSION_RANGE } from './bridge.js'
1313
import { getPlatformTarget } from './platform.js'
1414

1515
const require = createRequire(import.meta.url)
1616
const archiver = require('archiver')
1717

1818
test('Downloads the Deno CLI on demand and caches it for subsequent calls', async () => {
19-
const latestVersion = semver.minVersion(DENO_VERSION_RANGE)?.version ?? ''
19+
const latestVersion = semver.minVersion(LEGACY_DENO_VERSION_RANGE)?.version ?? ''
2020
const mockBinaryOutput = `#!/usr/bin/env sh\n\necho "deno ${latestVersion}"`
2121
const data = new PassThrough()
2222
const archive = archiver('zip', { zlib: { level: 9 } })

packages/edge-bundler/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
"deno/**",
1010
"!deno/**/*.test.ts",
1111
"dist/**/*.js",
12+
"!dist/**/*.test.js",
1213
"dist/**/*.d.ts",
1314
"shared/**"
1415
],

0 commit comments

Comments
 (0)