diff --git a/src/build/functions/edge.ts b/src/build/functions/edge.ts index 6d9919c11f..f6d331ddb6 100644 --- a/src/build/functions/edge.ts +++ b/src/build/functions/edge.ts @@ -1,5 +1,6 @@ import { cp, mkdir, readFile, rm, writeFile } from 'node:fs/promises' -import { dirname, join } from 'node:path' +import { dirname, join, relative, sep } from 'node:path' +import { sep as posixSep } from 'node:path/posix' import type { Manifest, ManifestFunction } from '@netlify/edge-functions' import { glob } from 'fast-glob' @@ -8,6 +9,8 @@ import { pathToRegexp } from 'path-to-regexp' import { EDGE_HANDLER_NAME, PluginContext } from '../plugin-context.js' +const toPosixPath = (path: string) => path.split(sep).join(posixSep) + const writeEdgeManifest = async (ctx: PluginContext, manifest: Manifest) => { await mkdir(ctx.edgeFunctionsDir, { recursive: true }) await writeFile(join(ctx.edgeFunctionsDir, 'manifest.json'), JSON.stringify(manifest, null, 2)) @@ -107,10 +110,19 @@ const copyHandlerDependencies = async ( const parts = [shim] + const outputFile = join(destDir, `server/${name}.js`) + if (wasm?.length) { - parts.push( - `import { decode as _base64Decode } from "../edge-runtime/vendor/deno.land/std@0.175.0/encoding/base64.ts";`, + const base64ModulePath = join( + destDir, + 'edge-runtime/vendor/deno.land/std@0.175.0/encoding/base64.ts', ) + + const base64ModulePathRelativeToOutputFile = toPosixPath( + relative(dirname(outputFile), base64ModulePath), + ) + + parts.push(`import { decode as _base64Decode } from "${base64ModulePathRelativeToOutputFile}";`) for (const wasmChunk of wasm ?? []) { const data = await readFile(join(srcDir, wasmChunk.filePath)) parts.push( @@ -126,9 +138,9 @@ const copyHandlerDependencies = async ( parts.push(`;// Concatenated file: ${file} \n`, entrypoint) } const exports = `const middlewareEntryKey = Object.keys(_ENTRIES).find(entryKey => entryKey.startsWith("middleware_${name}")); export default _ENTRIES[middlewareEntryKey].default;` - await mkdir(dirname(join(destDir, `server/${name}.js`)), { recursive: true }) + await mkdir(dirname(outputFile), { recursive: true }) - await writeFile(join(destDir, `server/${name}.js`), [...parts, exports].join('\n')) + await writeFile(outputFile, [...parts, exports].join('\n')) } const createEdgeHandler = async (ctx: PluginContext, definition: NextDefinition): Promise => { diff --git a/tests/fixtures/wasm-src/next.config.js b/tests/fixtures/wasm-src/next.config.js new file mode 100644 index 0000000000..f7630c0194 --- /dev/null +++ b/tests/fixtures/wasm-src/next.config.js @@ -0,0 +1,30 @@ +const { platform } = require('process') +const fsPromises = require('fs/promises') + +// Next.js uses `fs.promises.copyFile` to copy files from `.next`to the `.next/standalone` directory +// It tries copying the same file twice in parallel. Unix is fine with that, but Windows fails +// with "Resource busy or locked", failing the build. +// We work around this by memoizing the copy operation, so that the second copy is a no-op. +// Tracked in TODO: report to Next.js folks +if (platform === 'win32') { + const copies = new Map() + + const originalCopy = fsPromises.copyFile + fsPromises.copyFile = (src, dest, mode) => { + const key = `${dest}:${src}` + const existingCopy = copies.get(key) + if (existingCopy) return existingCopy + + const copy = originalCopy(src, dest, mode) + copies.set(key, copy) + return copy + } +} + +/** @type {import('next').NextConfig} */ +module.exports = { + output: 'standalone', + eslint: { + ignoreDuringBuilds: true, + }, +} diff --git a/tests/fixtures/wasm-src/package.json b/tests/fixtures/wasm-src/package.json new file mode 100644 index 0000000000..d5a533479f --- /dev/null +++ b/tests/fixtures/wasm-src/package.json @@ -0,0 +1,16 @@ +{ + "name": "og-api", + "version": "0.1.0", + "private": true, + "scripts": { + "postinstall": "next build", + "dev": "next dev", + "build": "next build" + }, + "dependencies": { + "@vercel/og": "latest", + "next": "latest", + "react": "18.2.0", + "react-dom": "18.2.0" + } +} diff --git a/tests/fixtures/wasm-src/src/add.wasm b/tests/fixtures/wasm-src/src/add.wasm new file mode 100755 index 0000000000..f22496d0b6 Binary files /dev/null and b/tests/fixtures/wasm-src/src/add.wasm differ diff --git a/tests/fixtures/wasm-src/src/app/og-node/route.js b/tests/fixtures/wasm-src/src/app/og-node/route.js new file mode 100644 index 0000000000..39638abb96 --- /dev/null +++ b/tests/fixtures/wasm-src/src/app/og-node/route.js @@ -0,0 +1,8 @@ +import { ImageResponse } from '@vercel/og' + +export async function GET() { + return new ImageResponse(
hi
, { + width: 1200, + height: 630, + }) +} diff --git a/tests/fixtures/wasm-src/src/app/og/route.js b/tests/fixtures/wasm-src/src/app/og/route.js new file mode 100644 index 0000000000..9304ca61e7 --- /dev/null +++ b/tests/fixtures/wasm-src/src/app/og/route.js @@ -0,0 +1,10 @@ +import { ImageResponse } from '@vercel/og' + +export async function GET() { + return new ImageResponse(
hi
, { + width: 1200, + height: 630, + }) +} + +export const runtime = 'edge' diff --git a/tests/fixtures/wasm-src/src/middleware.js b/tests/fixtures/wasm-src/src/middleware.js new file mode 100644 index 0000000000..3173be4c63 --- /dev/null +++ b/tests/fixtures/wasm-src/src/middleware.js @@ -0,0 +1,16 @@ +import wasm from './add.wasm?module' +const instance$ = WebAssembly.instantiate(wasm) + +async function increment(a) { + const { instance } = await instance$ + return instance.exports.add_one(a) +} +export default async function middleware(request) { + const input = Number(request.nextUrl.searchParams.get('input')) || 1 + const value = await increment(input) + return new Response(null, { headers: { data: JSON.stringify({ input, value }) } }) +} + +export const config = { + matcher: '/wasm', +} diff --git a/tests/fixtures/wasm-src/src/pages/api/og-wrong-runtime.js b/tests/fixtures/wasm-src/src/pages/api/og-wrong-runtime.js new file mode 100644 index 0000000000..a693c6f5df --- /dev/null +++ b/tests/fixtures/wasm-src/src/pages/api/og-wrong-runtime.js @@ -0,0 +1,22 @@ +// /pages/api/og.jsx +import { ImageResponse } from '@vercel/og' + +export default function () { + return new ImageResponse( + ( +
+ Hello! +
+ ), + ) +} diff --git a/tests/fixtures/wasm-src/src/pages/api/og.js b/tests/fixtures/wasm-src/src/pages/api/og.js new file mode 100644 index 0000000000..55ab54d2c1 --- /dev/null +++ b/tests/fixtures/wasm-src/src/pages/api/og.js @@ -0,0 +1,26 @@ +// /pages/api/og.jsx +import { ImageResponse } from '@vercel/og' + +export const config = { + runtime: 'edge', +} + +export default function () { + return new ImageResponse( + ( +
+ Hello! +
+ ), + ) +} diff --git a/tests/fixtures/wasm-src/src/pages/index.js b/tests/fixtures/wasm-src/src/pages/index.js new file mode 100644 index 0000000000..ff7159d914 --- /dev/null +++ b/tests/fixtures/wasm-src/src/pages/index.js @@ -0,0 +1,3 @@ +export default function Page() { + return

hello world

+} diff --git a/tests/integration/wasm.test.ts b/tests/integration/wasm.test.ts index fcad0dfbe6..679117de61 100644 --- a/tests/integration/wasm.test.ts +++ b/tests/integration/wasm.test.ts @@ -21,7 +21,10 @@ beforeEach(async (ctx) => { await startMockBlobStore(ctx) }) -describe('WASM', () => { +describe.each([ + { fixture: 'wasm', edgeHandlerFunction: '___netlify-edge-handler-middleware' }, + { fixture: 'wasm-src', edgeHandlerFunction: '___netlify-edge-handler-src-middleware' }, +])('$fixture', ({ fixture, edgeHandlerFunction }) => { beforeEach(async (ctx) => { // set for each test a new deployID and siteID ctx.deployID = generateRandomObjectID() @@ -33,7 +36,7 @@ describe('WASM', () => { await startMockBlobStore(ctx) - await createFixture('wasm', ctx) + await createFixture(fixture, ctx) await runPlugin(ctx) }) @@ -58,7 +61,7 @@ describe('WASM', () => { test('should work in middleware', async (ctx) => { const origin = new LocalServer() const response = await invokeEdgeFunction(ctx, { - functions: ['___netlify-edge-handler-middleware'], + functions: [edgeHandlerFunction], origin, url: '/wasm?input=3', })