Skip to content

refactor: extract edge runtime shims into separate file #1916

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Feb 3, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
63 changes: 6 additions & 57 deletions packages/runtime/src/helpers/edge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { resolve, join } from 'path'
import type { NetlifyConfig, NetlifyPluginConstants } from '@netlify/build'
import { greenBright } from 'chalk'
import destr from 'destr'
import { copy, copyFile, emptyDir, ensureDir, readJSON, readJson, writeJSON, writeJson } from 'fs-extra'
import { copy, copyFile, emptyDir, ensureDir, readJSON, writeJSON, writeJson } from 'fs-extra'

Choose a reason for hiding this comment

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

I just notice there’s the same function name twice in fs-extra but with different casings. Odd.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, I think that it's a legacy thing. Confusing, as I'm not sure which is "preferred"

import type { PrerenderManifest } from 'next/dist/build'
import type { MiddlewareManifest } from 'next/dist/build/webpack/plugins/middleware-plugin'
import type { RouteHas } from 'next/dist/lib/load-custom-routes'
Expand Down Expand Up @@ -68,7 +68,7 @@ export interface FunctionManifest {

const maybeLoadJson = <T>(path: string): Promise<T> | null => {
if (existsSync(path)) {
return readJson(path)
return readJSON(path)
}
}
export const isAppDirRoute = (route: string, appPathRoutesManifest: Record<string, string> | null): boolean =>
Expand All @@ -88,60 +88,6 @@ export const loadPrerenderManifest = (netlifyConfig: NetlifyConfig): Promise<Pre
*/
const sanitizeName = (name: string) => `next_${name.replace(/\W/g, '_')}`

/**
* Initialization added to the top of the edge function bundle
*/
const preamble = /* js */ `
import {
decode as _base64Decode,
} from "https://deno.land/std@0.175.0/encoding/base64.ts";

import { AsyncLocalStorage } from "https://deno.land/std@0.175.0/node/async_hooks.ts";

// Deno defines "window", but naughty libraries think this means it's a browser
delete globalThis.window
globalThis.process = { env: {...Deno.env.toObject(), NEXT_RUNTIME: 'edge', 'NEXT_PRIVATE_MINIMAL_MODE': '1' } }
globalThis.EdgeRuntime = "netlify-edge"
let _ENTRIES = {}

// Next.js expects this as a global
globalThis.AsyncLocalStorage = AsyncLocalStorage

// Next.js uses this extension to the Headers API implemented by Cloudflare workerd
if(!('getAll' in Headers.prototype)) {
Headers.prototype.getAll = function getAll(name) {
name = name.toLowerCase();
if (name !== "set-cookie") {
throw new Error("Headers.getAll is only supported for Set-Cookie");
}
return [...this.entries()]
.filter(([key]) => key === name)
.map(([, value]) => value);
};
}
// Next uses blob: urls to refer to local assets, so we need to intercept these
const _fetch = globalThis.fetch
const fetch = async (url, init) => {
try {
if (typeof url === 'object' && url.href?.startsWith('blob:')) {
const key = url.href.slice(5)
if (key in _ASSETS) {
return new Response(_base64Decode(_ASSETS[key]))
}
}
return await _fetch(url, init)
} catch (error) {
console.error(error)
throw error
}
}

// Next edge runtime uses "self" as a function-scoped global-like object, but some of the older polyfills expect it to equal globalThis
// See https://nextjs.org/docs/basic-features/supported-browsers-features#polyfills
const self = { ...globalThis, fetch }

`

// Slightly different spacing in different versions!
const IMPORT_UNSUPPORTED = [
`Object.defineProperty(globalThis,"__import_unsupported"`,
Expand All @@ -158,7 +104,10 @@ const getMiddlewareBundle = async ({
netlifyConfig: NetlifyConfig
}): Promise<string> => {
const { publish } = netlifyConfig.build
const chunks: Array<string> = [preamble]

const shims = await fs.readFile(getEdgeTemplatePath('shims.ts'), 'utf8')

const chunks: Array<string> = [shims]

chunks.push(`export const _DEFINITION = ${JSON.stringify(edgeFunctionDefinition)}`)

Expand Down
59 changes: 59 additions & 0 deletions packages/runtime/src/templates/edge/shims.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// deno-lint-ignore-file no-var prefer-const no-unused-vars no-explicit-any
import { decode as _base64Decode } from 'https://deno.land/std@0.175.0/encoding/base64.ts'
import { AsyncLocalStorage as ALSCompat } from 'https://deno.land/std@0.175.0/node/async_hooks.ts'

/**
* These are the shims, polyfills and other kludges to make Next.js work in standards-compliant runtime.
* This file isn't imported, but is instead inlined along with other chunks into the edge bundle.
*/

declare global {
var process: {
env: Record<string, string>
}
var EdgeRuntime: string
var AsyncLocalStorage: typeof ALSCompat
var _ASSETS: Record<string, string>
}

// Deno defines "window", but naughty libraries think this means it's a browser
delete (globalThis as Omit<typeof globalThis, 'window'> & Pick<Partial<typeof globalThis>, 'window'>).window
globalThis.process = {
env: { ...Deno.env.toObject(), NEXT_RUNTIME: 'edge', NEXT_PRIVATE_MINIMAL_MODE: '1' },
}
globalThis.EdgeRuntime = 'netlify-edge'
let _ENTRIES = {}

// Next.js expects this as a global
globalThis.AsyncLocalStorage = ALSCompat

// Next.js uses this extension to the Headers API implemented by Cloudflare workerd
if (!('getAll' in Headers.prototype)) {
;(Headers as any).prototype.getAll = function getAll(name: string) {
name = name.toLowerCase()
if (name !== 'set-cookie') {
throw new Error('Headers.getAll is only supported for Set-Cookie')
}
return [...this.entries()].filter(([key]) => key === name).map(([, value]) => value)
}
}
// Next uses blob: urls to refer to local assets, so we need to intercept these
const _fetch = globalThis.fetch
const fetch: typeof globalThis.fetch = async (url, init) => {
try {
if (url instanceof URL && url.href?.startsWith('blob:')) {
const key = url.href.slice(5)
if (key in _ASSETS) {
return new Response(_base64Decode(_ASSETS[key]))
}
}
return await _fetch(url, init)
} catch (error) {
console.error(error)
throw error
}
}

// Next edge runtime uses "self" as a function-scoped global-like object, but some of the older polyfills expect it to equal globalThis
// See https://nextjs.org/docs/basic-features/supported-browsers-features#polyfills
const self = { ...globalThis, fetch }