From 38eb0bca04ef57a7ff821ece03f16ed056e30881 Mon Sep 17 00:00:00 2001 From: Chris Barber Date: Thu, 13 Apr 2023 12:59:47 -0500 Subject: [PATCH] [go] Support 'go.work' file and resolve shared deps relative to work path (#9708) This PR fixes a few issues related to `vc dev`. 1. Create a default `go.work` file in the cache dir when building the `vercel-dev-server-go` executable 2. Copy the existing `go.mod` file into the cache dir and update any "replace" relative paths 3. Split the "build" logic into separate functions for the legacy "main" package build and the `go.mod` build Additionally, it fixes: 1. `vc build`: pass in `build.env` from `vercel.json` 2. Fix several tests to work with `vc dev` and `vc build` Linear: https://linear.app/vercel/issue/VCCLI-638/vc-dev-go-builder-cant-resolve-workspace-dependencies The user that reported the issue has tested this build and seems to fix their use case: https://github.com/vercel/vercel/issues/9393#issuecomment-1490726785. --- .../fixtures/go-work-with-shared/api/go.mod | 3 + .../fixtures/go-work-with-shared/api/index.go | 12 + .../dev/fixtures/go-work-with-shared/go.work | 6 + .../fixtures/go-work-with-shared/mylib/go.mod | 3 + .../go-work-with-shared/mylib/main.go | 9 + packages/cli/test/dev/integration-2.test.ts | 7 + packages/go/go-helpers.ts | 162 +++- packages/go/index.ts | 874 ++++++++++-------- .../go/test/fixtures/01-cowsay/vercel.json | 4 +- .../go/test/fixtures/02-parallel/api/go.mod | 2 +- .../test/fixtures/12-go-mod-subs/sub-1/go.mod | 2 +- .../test/fixtures/12-go-mod-subs/sub-2/go.mod | 2 +- .../14-go-mod-sub/other-folder/go.mod | 2 +- .../test/fixtures/16-custom-flag/probes.json | 8 + .../test/fixtures/16-custom-flag/vercel.json | 8 +- .../go/test/fixtures/17-go-mod-api/api/go.mod | 8 +- .../{vercel.json => probes.json} | 1 - .../26-go-work-with-shared/api/go.mod | 3 + .../26-go-work-with-shared/api/index.go | 12 + .../fixtures/26-go-work-with-shared/go.work | 6 + .../26-go-work-with-shared/mylib/go.mod | 3 + .../26-go-work-with-shared/mylib/main.go | 9 + .../26-go-work-with-shared/probes.json | 8 + 23 files changed, 729 insertions(+), 425 deletions(-) create mode 100644 packages/cli/test/dev/fixtures/go-work-with-shared/api/go.mod create mode 100644 packages/cli/test/dev/fixtures/go-work-with-shared/api/index.go create mode 100644 packages/cli/test/dev/fixtures/go-work-with-shared/go.work create mode 100644 packages/cli/test/dev/fixtures/go-work-with-shared/mylib/go.mod create mode 100644 packages/cli/test/dev/fixtures/go-work-with-shared/mylib/main.go create mode 100644 packages/go/test/fixtures/16-custom-flag/probes.json rename packages/go/test/fixtures/17-go-mod-api/{vercel.json => probes.json} (90%) create mode 100644 packages/go/test/fixtures/26-go-work-with-shared/api/go.mod create mode 100644 packages/go/test/fixtures/26-go-work-with-shared/api/index.go create mode 100644 packages/go/test/fixtures/26-go-work-with-shared/go.work create mode 100644 packages/go/test/fixtures/26-go-work-with-shared/mylib/go.mod create mode 100644 packages/go/test/fixtures/26-go-work-with-shared/mylib/main.go create mode 100644 packages/go/test/fixtures/26-go-work-with-shared/probes.json diff --git a/packages/cli/test/dev/fixtures/go-work-with-shared/api/go.mod b/packages/cli/test/dev/fixtures/go-work-with-shared/api/go.mod new file mode 100644 index 000000000..28f8032d4 --- /dev/null +++ b/packages/cli/test/dev/fixtures/go-work-with-shared/api/go.mod @@ -0,0 +1,3 @@ +module go-work-with-shared/api + +go 1.20 diff --git a/packages/cli/test/dev/fixtures/go-work-with-shared/api/index.go b/packages/cli/test/dev/fixtures/go-work-with-shared/api/index.go new file mode 100644 index 000000000..495cf883c --- /dev/null +++ b/packages/cli/test/dev/fixtures/go-work-with-shared/api/index.go @@ -0,0 +1,12 @@ +package handler + +import ( + "fmt" + "net/http" + "go-work-with-shared/mylib" +) + +// Handler function +func Handler(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, mylib.Say("hello")) +} diff --git a/packages/cli/test/dev/fixtures/go-work-with-shared/go.work b/packages/cli/test/dev/fixtures/go-work-with-shared/go.work new file mode 100644 index 000000000..3bf0d6e37 --- /dev/null +++ b/packages/cli/test/dev/fixtures/go-work-with-shared/go.work @@ -0,0 +1,6 @@ +go 1.20 + +use ( + ./api/ + ./mylib/ +) diff --git a/packages/cli/test/dev/fixtures/go-work-with-shared/mylib/go.mod b/packages/cli/test/dev/fixtures/go-work-with-shared/mylib/go.mod new file mode 100644 index 000000000..e7010fef8 --- /dev/null +++ b/packages/cli/test/dev/fixtures/go-work-with-shared/mylib/go.mod @@ -0,0 +1,3 @@ +module go-work-with-shared/mylib + +go 1.20 diff --git a/packages/cli/test/dev/fixtures/go-work-with-shared/mylib/main.go b/packages/cli/test/dev/fixtures/go-work-with-shared/mylib/main.go new file mode 100644 index 000000000..1a975cf31 --- /dev/null +++ b/packages/cli/test/dev/fixtures/go-work-with-shared/mylib/main.go @@ -0,0 +1,9 @@ +package mylib + +import ( + "runtime" +) + +func Say(text string) string { + return text + ":" + runtime.Version() +} diff --git a/packages/cli/test/dev/integration-2.test.ts b/packages/cli/test/dev/integration-2.test.ts index 3827e8bf5..a64a2f309 100644 --- a/packages/cli/test/dev/integration-2.test.ts +++ b/packages/cli/test/dev/integration-2.test.ts @@ -182,6 +182,13 @@ test( }) ); +test( + '[vercel dev] Should support `*.go` API serverless functions with `go.work` and lib', + testFixtureStdio('go-work-with-shared', async (testPath: any) => { + await testPath(200, `/api`, 'hello:go1.20.2'); + }) +); + test( '[vercel dev] Should set the `ts-node` "target" to match Node.js version', testFixtureStdio('node-ts-node-target', async (testPath: any) => { diff --git a/packages/go/go-helpers.ts b/packages/go/go-helpers.ts index 5daf4ca1d..44773ef44 100644 --- a/packages/go/go-helpers.ts +++ b/packages/go/go-helpers.ts @@ -9,7 +9,7 @@ import { remove, symlink, } from 'fs-extra'; -import { join, delimiter, dirname } from 'path'; +import { delimiter, dirname, join } from 'path'; import stringArgv from 'string-argv'; import { cloneEnv, debug } from '@vercel/build-utils'; import { pipeline } from 'stream'; @@ -22,7 +22,7 @@ import type { Env } from '@vercel/build-utils'; const streamPipeline = promisify(pipeline); const versionMap = new Map([ - ['1.20', '1.20.1'], + ['1.20', '1.20.2'], ['1.19', '1.19.6'], ['1.18', '1.18.10'], ['1.17', '1.17.13'], @@ -36,17 +36,32 @@ const archMap = new Map([ ['x86', '386'], ]); const platformMap = new Map([['win32', 'windows']]); -export const cacheDir = join('.vercel', 'cache', 'golang'); +export const localCacheDir = join('.vercel', 'cache', 'golang'); + const GO_FLAGS = process.platform === 'win32' ? [] : ['-ldflags', '-s -w']; const GO_MIN_VERSION = 13; -const getPlatform = (p: string) => platformMap.get(p) || p; -const getArch = (a: string) => archMap.get(a) || a; +/** + * Determines the URL to download the Golang SDK. + * @param version The desireed Go version + * @returns The Go download URL + */ function getGoUrl(version: string) { const { arch, platform } = process; - const goArch = getArch(arch); - const goPlatform = getPlatform(platform); const ext = platform === 'win32' ? 'zip' : 'tar.gz'; + const goPlatform = platformMap.get(platform) || platform; + let goArch = archMap.get(arch) || arch; + + // Go 1.16 was the first version to support arm64, so if the version is older + // we need to download the amd64 version + if ( + platform === 'darwin' && + goArch === 'arm64' && + parseInt(version.split('.')[1], 10) < 16 + ) { + goArch = 'amd64'; + } + const filename = `go${version}.${goPlatform}-${goArch}.${ext}`; return { filename, @@ -61,32 +76,91 @@ export const goGlobalCachePath = join( export const OUT_EXTENSION = process.platform === 'win32' ? '.exe' : ''; -export async function getAnalyzedEntrypoint( - workPath: string, - filePath: string, - modulePath: string -) { - const bin = join(__dirname, `analyze${OUT_EXTENSION}`); - - const isAnalyzeExist = await pathExists(bin); - if (!isAnalyzeExist) { - debug(`Building analyze bin: ${bin}`); - const src = join(__dirname, 'util', 'analyze.go'); - const go = await createGo({ - modulePath, - workPath, - }); - await go.build(src, bin); - } - - debug(`Analyzing entrypoint ${filePath} with modulePath ${modulePath}`); - const args = [`-modpath=${modulePath}`, filePath]; - const analyzed = await execa.stdout(bin, args); - debug(`Analyzed entrypoint ${analyzed}`); - return analyzed; +interface Analyzed { + functionName: string; + packageName: string; + watch?: boolean; } -class GoWrapper { +/** + * Parses the AST of the specified entrypoint Go file. + * @param workPath The work path (e.g. `/path/to/project`) + * @param entrypoint The path to the entrypoint file (e.g. + * `/path/to/project/api/index.go`) + * @param modulePath The path to the directory containing the `go.mod` (e.g. + * `/path/to/project/api`) + * @returns The results from the AST parsing + */ +export async function getAnalyzedEntrypoint({ + entrypoint, + modulePath, + workPath, +}: { + entrypoint: string; + modulePath?: string; + workPath: string; +}): Promise { + const bin = join(__dirname, `analyze${OUT_EXTENSION}`); + let analyzed: string; + + try { + // build the `analyze` binary if not found in the `dist` directory + const isAnalyzeExist = await pathExists(bin); + if (!isAnalyzeExist) { + debug(`Building analyze bin: ${bin}`); + const src = join(__dirname, 'util', 'analyze.go'); + let go; + const createOpts = { + modulePath, + opts: { cwd: __dirname }, + workPath, + }; + try { + go = await createGo(createOpts); + } catch (err) { + // if the version in the `go.mod` is too old, then download the latest + if ( + err instanceof GoError && + err.code === 'ERR_UNSUPPORTED_GO_VERSION' + ) { + delete createOpts.modulePath; + go = await createGo(createOpts); + } else { + throw err; + } + } + await go.build(src, bin); + } + } catch (err) { + console.error('Failed to build the Go AST analyzer'); + throw err; + } + + try { + debug(`Analyzing entrypoint ${entrypoint} with modulePath ${modulePath}`); + const args = [`-modpath=${modulePath}`, join(workPath, entrypoint)]; + analyzed = await execa.stdout(bin, args); + } catch (err) { + console.error(`Failed to parse AST for "${entrypoint}"`); + throw err; + } + + debug(`Analyzed entrypoint ${analyzed}`); + + if (!analyzed) { + const err = new Error( + `Could not find an exported function in "${entrypoint}" +Learn more: https://vercel.com/docs/runtimes#official-runtimes/go + ` + ); + console.error(err.message); + throw err; + } + + return JSON.parse(analyzed) as Analyzed; +} + +export class GoWrapper { private env: Env; private opts: execa.Options; @@ -101,10 +175,11 @@ class GoWrapper { private execute(...args: string[]) { const { opts, env } = this; debug( - `Exec: go ${args - .map(a => (a.includes(' ') ? `"${a}"` : a)) - .join(' ')} CWD=${opts.cwd}` + `Exec: go ${args.map(a => (a.includes(' ') ? `"${a}"` : a)).join(' ')}` ); + debug(` CWD=${opts.cwd}`); + debug(` GOROOT=${(env || opts.env).GOROOT}`); + debug(` GO_BUILD_FLAGS=${(env || opts.env).GO_BUILD_FLAGS}`); return execa('go', args, { stdio: 'inherit', ...opts, env }); } @@ -127,9 +202,8 @@ class GoWrapper { debug(`Building optimized 'go' binary ${src} -> ${dest}`); const sources = Array.isArray(src) ? src : [src]; - const flags = process.env.GO_BUILD_FLAGS - ? stringArgv(process.env.GO_BUILD_FLAGS) - : GO_FLAGS; + const envGoBuildFlags = (this.env || this.opts.env).GO_BUILD_FLAGS; + const flags = envGoBuildFlags ? stringArgv(envGoBuildFlags) : GO_FLAGS; return this.execute('build', ...flags, '-o', dest, ...sources); } @@ -186,7 +260,7 @@ export async function createGo({ goGlobalCachePath, `${goSelectedVersion}_${platform}_${process.arch}` ); - const goCacheDir = join(workPath, cacheDir); + const goCacheDir = join(workPath, localCacheDir); if (goPreferredVersion) { debug(`Preferred go version ${goPreferredVersion} (from go.mod)`); @@ -234,7 +308,7 @@ export async function createGo({ debug(`Found go ${version} in ${label}, but version is unsupported`); } if (version === goSelectedVersion || short === goSelectedVersion) { - console.log(`Selected go ${version} (from ${label})`); + debug(`Selected go ${version} (from ${label})`); await setGoEnv(goDir); return new GoWrapper(env, opts); @@ -339,6 +413,10 @@ function parseGoVersionString(goVersionOutput: string) { }; } +class GoError extends Error { + code: string | undefined; +} + /** * Attempts to parse the preferred Go version from the `go.mod` file. * @@ -359,6 +437,12 @@ async function parseGoModVersion( const full = versionMap.get(`${major}.${minor}`); if (major === 1 && minor >= GO_MIN_VERSION && full) { version = full; + } else if (!isNaN(minor)) { + const err = new GoError( + `Unsupported Go version ${major}.${minor} in ${file}` + ); + err.code = 'ERR_UNSUPPORTED_GO_VERSION'; + throw err; } else { console.log(`Warning: Unknown Go version in ${file}`); } diff --git a/packages/go/index.ts b/packages/go/index.ts index 53c49f900..f6d24271d 100644 --- a/packages/go/index.ts +++ b/packages/go/index.ts @@ -4,7 +4,7 @@ import { homedir, tmpdir } from 'os'; import { spawn } from 'child_process'; import { Readable } from 'stream'; import once from '@tootallnate/once'; -import { join, dirname, basename, normalize, posix, sep } from 'path'; +import { basename, dirname, join, normalize, posix, relative } from 'path'; import { readFile, writeFile, @@ -17,6 +17,7 @@ import { rmdir, readdir, unlink, + copy, } from 'fs-extra'; import { BuildOptions, @@ -26,7 +27,7 @@ import { StartDevServerResult, glob, download, - createLambda, + Lambda, getWriteableDirectory, shouldServe, debug, @@ -36,21 +37,20 @@ import { const TMP = tmpdir(); import { - cacheDir, + localCacheDir, createGo, getAnalyzedEntrypoint, + GoWrapper, OUT_EXTENSION, } from './go-helpers'; -const handlerFileName = `handler${OUT_EXTENSION}`; - export { shouldServe }; -interface Analyzed { - functionName: string; - packageName: string; - watch?: boolean; -} +// in order to allow the user to have `main.go`, +// we need our `main.go` to be called something else +const MAIN_GO_FILENAME = 'main__vc__go__.go'; + +const HANDLER_FILENAME = `handler${OUT_EXTENSION}`; interface PortInfo { port: number; @@ -100,6 +100,12 @@ type UndoFunctionRename = { to: string; }; +type UndoActions = { + fileActions: UndoFileAction[]; + directoryCreation: string[]; + functionRenames: UndoFunctionRename[]; +}; + export const version = 3; export async function build({ @@ -117,17 +123,24 @@ export async function build({ // keep track of file system actions we need to undo // the keys "from" and "to" refer to what needs to be done // in order to undo the action, not what the original action was - const undoFileActions: UndoFileAction[] = []; - const undoDirectoryCreation: string[] = []; - const undoFunctionRenames: UndoFunctionRename[] = []; + const undo: UndoActions = { + fileActions: [], + directoryCreation: [], + functionRenames: [], + }; + + const env = cloneEnv(process.env, meta.env, { + GOARCH: 'amd64', + GOOS: 'linux', + }); try { - if (process.env.GIT_CREDENTIALS) { + if (env.GIT_CREDENTIALS) { debug('Initialize Git credentials...'); - await initPrivateGit(process.env.GIT_CREDENTIALS); + await initPrivateGit(env.GIT_CREDENTIALS); } - if (process.env.GO111MODULE) { + if (env.GO111MODULE) { console.log(`\nManually assigning 'GO111MODULE' is not recommended. By default: @@ -143,7 +156,7 @@ export async function build({ const renamedEntrypoint = getRenamedEntrypoint(entrypoint); if (renamedEntrypoint) { await move(join(workPath, entrypoint), join(workPath, renamedEntrypoint)); - undoFileActions.push({ + undo.fileActions.push({ to: join(workPath, entrypoint), from: join(workPath, renamedEntrypoint), }); @@ -151,113 +164,32 @@ export async function build({ } const entrypointAbsolute = join(workPath, entrypoint); - const entrypointArr = entrypoint.split(posix.sep); - - debug(`Parsing AST for "${entrypoint}"`); - let analyzed: string; - try { - const goModAbsPath = await findGoModPath(workPath); - if (goModAbsPath) { - debug(`Found ${goModAbsPath}"`); - } - - analyzed = await getAnalyzedEntrypoint( - workPath, - entrypointAbsolute, - dirname(goModAbsPath) - ); - } catch (err) { - console.error(`Failed to parse AST for "${entrypoint}"`); - throw err; - } - - if (!analyzed) { - const err = new Error( - `Could not find an exported function in "${entrypoint}" - Learn more: https://vercel.com/docs/runtimes#official-runtimes/go - ` - ); - console.error(err.message); - throw err; - } - - const parsedAnalyzed = JSON.parse(analyzed) as Analyzed; - - // find `go.mod` in modFiles const entrypointDirname = dirname(entrypointAbsolute); - let isGoModExist = false; - let goModPath = ''; - let isGoModInRootDir = false; - const modFileRefs = await glob('**/*.mod', workPath); - const modFiles = Object.keys(modFileRefs); + const { goModPath, isGoModInRootDir } = await findGoModPath( + entrypointDirname, + workPath + ); - for (const file of modFiles) { - const fileDirname = dirname(file); - if (file === 'go.mod') { - isGoModExist = true; - isGoModInRootDir = true; - goModPath = join(workPath, fileDirname); - } else if (file.endsWith('go.mod')) { - if (entrypointDirname === fileDirname) { - isGoModExist = true; - goModPath = join(workPath, fileDirname); - - debug(`Found file dirname equals entrypoint dirname: ${fileDirname}`); - break; - } - - if (!isGoModInRootDir && config.zeroConfig && file === 'api/go.mod') { - // We didn't find `/go.mod` but we found `/api/go.mod` so move it to the root - isGoModExist = true; - isGoModInRootDir = true; - goModPath = join(fileDirname, '..'); - const pathParts = file.split(sep); - pathParts.pop(); // Remove go.mod - pathParts.pop(); // Remove api - pathParts.push('go.mod'); - - const newRoot = pathParts.join(sep); - const newFsPath = join(workPath, newRoot); - - debug(`Moving api/go.mod to root: ${file} to ${newFsPath}`); - await move(file, newFsPath); - undoFileActions.push({ - to: file, - from: newFsPath, - }); - - const oldSumPath = join(dirname(file), 'go.sum'); - const newSumPath = join(dirname(newFsPath), 'go.sum'); - if (await pathExists(oldSumPath)) { - debug(`Moving api/go.sum to root: ${oldSumPath} to ${newSumPath}`); - await move(oldSumPath, newSumPath); - undoFileActions.push({ - to: oldSumPath, - from: newSumPath, - }); - } - break; - } - } + if (!goModPath && (await pathExists(join(workPath, 'vendor')))) { + throw new Error('`go.mod` is required to use a `vendor` directory.'); } - const input = entrypointDirname; - const includedFiles: Files = {}; + const analyzed = await getAnalyzedEntrypoint({ + entrypoint, + modulePath: goModPath ? dirname(goModPath) : undefined, + workPath, + }); - if (config && config.includeFiles) { - const patterns = Array.isArray(config.includeFiles) - ? config.includeFiles - : [config.includeFiles]; - for (const pattern of patterns) { - const fsFiles = await glob(pattern, input); - for (const [assetName, asset] of Object.entries(fsFiles)) { - includedFiles[assetName] = asset; - } - } + // check if package name other than main + // using `go.mod` way building the handler + const packageName = analyzed.packageName; + if (goModPath && packageName === 'main') { + throw new Error('Please change `package main` to `package handler`'); } - const originalFunctionName = parsedAnalyzed.functionName; + // rename the Go handler function name in the original entrypoint file + const originalFunctionName = analyzed.functionName; const handlerFunctionName = getNewHandlerFunctionName( originalFunctionName, entrypoint @@ -267,228 +199,59 @@ export async function build({ originalFunctionName, handlerFunctionName ); - undoFunctionRenames.push({ + undo.functionRenames.push({ fsPath: originalEntrypointAbsolute, from: handlerFunctionName, to: originalFunctionName, }); - if (!isGoModExist) { - if (await pathExists(join(workPath, 'vendor'))) { - throw new Error('`go.mod` is required to use a `vendor` directory.'); + const includedFiles: Files = {}; + if (config && config.includeFiles) { + const patterns = Array.isArray(config.includeFiles) + ? config.includeFiles + : [config.includeFiles]; + for (const pattern of patterns) { + const fsFiles = await glob(pattern, entrypointDirname); + for (const [assetName, asset] of Object.entries(fsFiles)) { + includedFiles[assetName] = asset; + } } } - // check if package name other than main - // using `go.mod` way building the handler - const packageName = parsedAnalyzed.packageName; - - if (isGoModExist && packageName === 'main') { - throw new Error('Please change `package main` to `package handler`'); - } - - const outDir = await getWriteableDirectory(); - - // in order to allow the user to have `main.go`, - // we need our `main.go` to be called something else - const mainGoFileName = 'main__vc__go__.go'; - + const modulePath = goModPath ? dirname(goModPath) : undefined; const go = await createGo({ - modulePath: goModPath, + modulePath, opts: { cwd: entrypointDirname, - env: { - GOARCH: 'amd64', - GOOS: 'linux', - }, + env, }, workPath, }); - if (packageName !== 'main') { - if (!isGoModExist) { - try { - const defaultGoModContent = `module ${packageName}`; + const outDir = await getWriteableDirectory(); + const buildOptions: BuildHandlerOptions = { + downloadPath, + entrypoint, + entrypointAbsolute, + entrypointDirname, + go, + goModPath, + handlerFunctionName, + isGoModInRootDir, + outDir, + packageName, + undo, + }; - await writeFile( - join(entrypointDirname, 'go.mod'), - defaultGoModContent - ); - - undoFileActions.push({ - to: undefined, // delete - from: join(entrypointDirname, 'go.mod'), - }); - - // remove the `go.sum` file that will be generated as well - undoFileActions.push({ - to: undefined, // delete - from: join(entrypointDirname, 'go.sum'), - }); - } catch (err) { - console.error(`Failed to create default go.mod for ${packageName}`); - throw err; - } - } - - const modMainGoContents = await readFile( - join(__dirname, 'main.go'), - 'utf8' - ); - - let goPackageName = `${packageName}/${packageName}`; - const goFuncName = `${packageName}.${handlerFunctionName}`; - - if (isGoModExist) { - const goModContents = await readFile(join(goModPath, 'go.mod'), 'utf8'); - const usrModName = goModContents.split('\n')[0].split(' ')[1]; - if (entrypointArr.length > 1 && isGoModInRootDir) { - const cleanPackagePath = [...entrypointArr]; - cleanPackagePath.pop(); - goPackageName = `${usrModName}/${cleanPackagePath.join('/')}`; - } else { - goPackageName = `${usrModName}/${packageName}`; - } - } - - const mainModGoContents = modMainGoContents - .replace('__VC_HANDLER_PACKAGE_NAME', goPackageName) - .replace('__VC_HANDLER_FUNC_NAME', goFuncName); - - if (isGoModExist && isGoModInRootDir) { - debug('[mod-root] Write main file to ' + downloadPath); - await writeFile(join(downloadPath, mainGoFileName), mainModGoContents); - - undoFileActions.push({ - to: undefined, // delete - from: join(downloadPath, mainGoFileName), - }); - } else if (isGoModExist && !isGoModInRootDir) { - debug('[mod-other] Write main file to ' + goModPath); - await writeFile(join(goModPath, mainGoFileName), mainModGoContents); - undoFileActions.push({ - to: undefined, // delete - from: join(goModPath, mainGoFileName), - }); - } else { - debug('[entrypoint] Write main file to ' + entrypointDirname); - await writeFile( - join(entrypointDirname, mainGoFileName), - mainModGoContents - ); - undoFileActions.push({ - to: undefined, // delete - from: join(entrypointDirname, mainGoFileName), - }); - } - - // move user go file to folder - try { - // default path - let finalDestination = join(entrypointDirname, packageName, entrypoint); - - // if `entrypoint` include folder, only use filename - if (entrypointArr.length > 1) { - finalDestination = join( - entrypointDirname, - packageName, - entrypointArr[entrypointArr.length - 1] - ); - } - - if (dirname(entrypointAbsolute) === goModPath || !isGoModExist) { - debug( - `moving entrypoint "${entrypointAbsolute}" to "${finalDestination}"` - ); - - await move(entrypointAbsolute, finalDestination); - undoFileActions.push({ - to: entrypointAbsolute, - from: finalDestination, - }); - undoDirectoryCreation.push(dirname(finalDestination)); - } - } catch (err) { - console.error('Failed to move entry to package folder'); - throw err; - } - - let baseGoModPath = ''; - if (isGoModExist && isGoModInRootDir) { - baseGoModPath = downloadPath; - } else if (isGoModExist && !isGoModInRootDir) { - baseGoModPath = goModPath; - } else { - baseGoModPath = entrypointDirname; - } - - debug('Tidy `go.mod` file...'); - try { - // ensure go.mod up-to-date - await go.mod(); - } catch (err) { - console.error('failed to `go mod tidy`'); - throw err; - } - - debug('Running `go build`...'); - const destPath = join(outDir, handlerFileName); - - try { - const src = [join(baseGoModPath, mainGoFileName)]; - - await go.build(src, destPath); - } catch (err) { - console.error('failed to `go build`'); - throw err; - } + if (packageName === 'main') { + await buildHandlerAsPackageMain(buildOptions); } else { - // legacy mode - // we need `main.go` in the same dir as the entrypoint, - // otherwise `go build` will refuse to build - const originalMainGoContents = await readFile( - join(__dirname, 'main.go'), - 'utf8' - ); - const mainGoContents = originalMainGoContents - .replace('"__VC_HANDLER_PACKAGE_NAME"', '') - .replace('__VC_HANDLER_FUNC_NAME', handlerFunctionName); - - // Go doesn't like to build files in different directories, - // so now we place `main.go` together with the user code - await writeFile(join(entrypointDirname, mainGoFileName), mainGoContents); - undoFileActions.push({ - to: undefined, // delete - from: join(entrypointDirname, mainGoFileName), - }); - - // `go get` will look at `*.go` (note we set `cwd`), parse the `import`s - // and download any packages that aren't part of the stdlib - debug('Running `go get`...'); - try { - await go.get(); - } catch (err) { - console.error('Failed to `go get`'); - throw err; - } - - debug('Running `go build`...'); - const destPath = join(outDir, handlerFileName); - try { - const src = [ - join(entrypointDirname, mainGoFileName), - entrypointAbsolute, - ].map(file => normalize(file)); - await go.build(src, destPath); - } catch (err) { - console.error('failed to `go build`'); - throw err; - } + await buildHandlerWithGoMod(buildOptions); } - const lambda = await createLambda({ + const lambda = new Lambda({ files: { ...(await glob('**', outDir)), ...includedFiles }, - handler: handlerFileName, + handler: HANDLER_FILENAME, runtime: 'go1.x', supportsWrapper: true, environment: {}, @@ -498,16 +261,12 @@ export async function build({ output: lambda, }; } catch (error) { - debug('Go Builder Error: ' + error); + debug(`Go Builder Error: ${error}`); throw error; } finally { try { - await cleanupFileSystem( - undoFileActions, - undoDirectoryCreation, - undoFunctionRenames - ); + await cleanupFileSystem(undo); } catch (error) { if (error instanceof Error) { console.error(`Build cleanup failed: ${error.message}`); @@ -517,6 +276,219 @@ export async function build({ } } +type BuildHandlerOptions = { + downloadPath: string; + entrypoint: string; + entrypointAbsolute: string; + entrypointDirname: string; + go: GoWrapper; + goModPath?: string; + handlerFunctionName: string; + isGoModInRootDir: boolean; + outDir: string; + packageName: string; + undo: UndoActions; +}; + +/** + * Build the Go function where the package name is not `"main"`. If a `go.mod` + * does not exist, a default one will be used. + */ +async function buildHandlerWithGoMod({ + downloadPath, + entrypoint, + entrypointAbsolute, + entrypointDirname, + go, + goModPath, + handlerFunctionName, + isGoModInRootDir, + outDir, + packageName, + undo, +}: BuildHandlerOptions): Promise { + debug( + `Building Go handler as package "${packageName}" (with${ + goModPath ? '' : 'out' + } go.mod)` + ); + + let goModDirname: string | undefined; + + if (goModPath !== undefined) { + goModDirname = dirname(goModPath); + + // first we backup the original + const backupFile = join(goModDirname, `__vc_go.mod.bak`); + await copy(goModPath, backupFile); + + undo.fileActions.push({ + to: goModPath, + from: backupFile, + }); + + const goSumPath = join(goModDirname, 'go.sum'); + const isGoSumExists = await pathExists(goSumPath); + if (!isGoSumExists) { + // remove the `go.sum` file that will be generated as well + undo.fileActions.push({ + to: undefined, // delete + from: goSumPath, + }); + } + } + + const entrypointArr = entrypoint.split(posix.sep); + let goPackageName = `${packageName}/${packageName}`; + const goFuncName = `${packageName}.${handlerFunctionName}`; + + // if we have a go.mod, determine the relative path of the entrypoint to the + // go.mod directory and use that for the import package name in main.go + const relPackagePath = goModDirname + ? posix.relative(goModDirname, entrypointDirname) + : ''; + if (relPackagePath) { + goPackageName = posix.join(packageName, relPackagePath); + } + + let mainGoFile: string; + if (goModPath && isGoModInRootDir) { + debug(`[mod-root] Write main file to ${downloadPath}`); + mainGoFile = join(downloadPath, MAIN_GO_FILENAME); + } else if (goModDirname && !isGoModInRootDir) { + debug(`[mod-other] Write main file to ${goModDirname}`); + mainGoFile = join(goModDirname, MAIN_GO_FILENAME); + } else { + debug(`[entrypoint] Write main file to ${entrypointDirname}`); + mainGoFile = join(entrypointDirname, MAIN_GO_FILENAME); + } + + await Promise.all([ + writeEntrypoint(mainGoFile, goPackageName, goFuncName), + writeGoMod({ + destDir: goModDirname ? goModDirname : entrypointDirname, + goModPath, + packageName, + }), + ]); + + undo.fileActions.push({ + to: undefined, // delete + from: mainGoFile, + }); + + // move user go file to folder + try { + // default path + let finalDestination = join(entrypointDirname, packageName, entrypoint); + + // if `entrypoint` include folder, only use filename + if (entrypointArr.length > 1) { + finalDestination = join( + entrypointDirname, + packageName, + entrypointArr[entrypointArr.length - 1] + ); + } + + if (!goModPath || dirname(entrypointAbsolute) === dirname(goModPath)) { + debug( + `moving entrypoint "${entrypointAbsolute}" to "${finalDestination}"` + ); + + await move(entrypointAbsolute, finalDestination); + undo.fileActions.push({ + to: entrypointAbsolute, + from: finalDestination, + }); + undo.directoryCreation.push(dirname(finalDestination)); + } + } catch (err) { + console.error('Failed to move entry to package folder'); + throw err; + } + + let baseGoModPath = ''; + if (goModPath && isGoModInRootDir) { + baseGoModPath = downloadPath; + } else if (goModPath && !isGoModInRootDir) { + baseGoModPath = dirname(goModPath); + } else { + baseGoModPath = entrypointDirname; + } + + debug('Tidy `go.mod` file...'); + try { + // ensure go.mod up-to-date + await go.mod(); + } catch (err) { + console.error('failed to `go mod tidy`'); + throw err; + } + + debug('Running `go build`...'); + const destPath = join(outDir, HANDLER_FILENAME); + + try { + const src = [join(baseGoModPath, MAIN_GO_FILENAME)]; + + await go.build(src, destPath); + } catch (err) { + console.error('failed to `go build`'); + throw err; + } +} + +/** + * Builds the wrapped Go function using the legacy mode where package name is + * `"main"` and we need `main.go` in the same dir as the entrypoint, otherwise + * `go build` will refuse to build. + */ +async function buildHandlerAsPackageMain({ + entrypointAbsolute, + entrypointDirname, + go, + handlerFunctionName, + outDir, + undo, +}: BuildHandlerOptions): Promise { + debug('Building Go handler as package "main" (legacy)'); + + await writeEntrypoint( + join(entrypointDirname, MAIN_GO_FILENAME), + '', + handlerFunctionName + ); + + undo.fileActions.push({ + to: undefined, // delete + from: join(entrypointDirname, MAIN_GO_FILENAME), + }); + + // `go get` will look at `*.go` (note we set `cwd`), parse the `import`s + // and download any packages that aren't part of the stdlib + debug('Running `go get`...'); + try { + await go.get(); + } catch (err) { + console.error('Failed to `go get`'); + throw err; + } + + debug('Running `go build`...'); + const destPath = join(outDir, HANDLER_FILENAME); + try { + const src = [ + join(entrypointDirname, MAIN_GO_FILENAME), + entrypointAbsolute, + ].map(file => normalize(file)); + await go.build(src, destPath); + } catch (err) { + console.error('failed to `go build`'); + throw err; + } +} + async function renameHandlerFunction(fsPath: string, from: string, to: string) { let fileContents = await readFile(fsPath, 'utf8'); @@ -560,17 +532,20 @@ export function getNewHandlerFunctionName( return newHandlerName; } -async function cleanupFileSystem( - undoFileActions: UndoFileAction[], - undoDirectoryCreation: string[], - undoFunctionRenames: UndoFunctionRename[] -) { +/** + * Remove any temporary files, directories, and file changes. + */ +async function cleanupFileSystem({ + fileActions, + directoryCreation, + functionRenames, +}: UndoActions) { // we have to undo the actions in reverse order in cases // where one file was moved multiple times, which happens // using files that start with brackets - for (const action of undoFileActions.reverse()) { + for (const action of fileActions.reverse()) { if (action.to) { - await move(action.from, action.to); + await move(action.from, action.to, { overwrite: true }); } else { await remove(action.from); } @@ -578,11 +553,11 @@ async function cleanupFileSystem( // after files are moved back, we can undo function renames // these reference the original file location - for (const rename of undoFunctionRenames) { + for (const rename of functionRenames) { await renameHandlerFunction(rename.fsPath, rename.from, rename.to); } - const undoDirectoryPromises = undoDirectoryCreation.map(async directory => { + const undoDirectoryPromises = directoryCreation.map(async directory => { const contents = await readdir(directory); // only delete an empty directory // if it has contents, either something went wrong during cleanup or this @@ -595,18 +570,34 @@ async function cleanupFileSystem( await Promise.all(undoDirectoryPromises); } -async function findGoModPath(workPath: string): Promise { - let checkPath = join(workPath, 'go.mod'); - if (await pathExists(checkPath)) { - return checkPath; +/** + * Attempts to find a `go.mod` starting in the entrypoint directory and + * scanning up the directory tree. + * @param entrypointDir The entrypoint directory (e.g. `/path/to/project/api`) + * @param workPath The work path (e.g. `/path/to/project`) + * @returns The absolute path to the `go.mod` and a flag if the `go.mod` is in + * the work path root + */ +async function findGoModPath(entrypointDir: string, workPath: string) { + let goModPath: string | undefined = undefined; + let isGoModInRootDir = false; + let dir = entrypointDir; + + while (!isGoModInRootDir) { + isGoModInRootDir = dir === workPath; + const goMod = join(dir, 'go.mod'); + if (await pathExists(goMod)) { + goModPath = goMod; + debug(`Found ${goModPath}"`); + break; + } + dir = dirname(dir); } - checkPath = join(workPath, 'api/go.mod'); - if (await pathExists(checkPath)) { - return checkPath; - } - - return ''; + return { + goModPath, + isGoModInRootDir, + }; } function isPortInfo(v: any): v is PortInfo { @@ -638,17 +629,159 @@ async function copyDevServer( await writeFile(join(dest, 'vercel-dev-server-main.go'), patched); } -async function writeDefaultGoMod( - entrypointDirname: string, - packageName: string +async function writeEntrypoint( + dest: string, + goPackageName: string, + goFuncName: string ) { - const defaultGoModContent = `module ${packageName}`; + const modMainGoContents = await readFile(join(__dirname, 'main.go'), 'utf8'); + const mainModGoContents = modMainGoContents + .replace('__VC_HANDLER_PACKAGE_NAME', goPackageName) + .replace('__VC_HANDLER_FUNC_NAME', goFuncName); + await writeFile(dest, mainModGoContents, 'utf-8'); +} - await writeFile( - join(entrypointDirname, 'go.mod'), - defaultGoModContent, - 'utf-8' - ); +/** + * Writes a `go.mod` file in the specified directory. If a `go.mod` file + * exists, then update the module name and any relative `replace` statements, + * otherwise write the minimum module name. + * @param workPath The work path; required if `goModPath` exists + * @param goModPath The path to the `go.mod`, or `undefined` if not found + * @param destDir The directory to write the `go.mod` to + * @param packageName The module name to inject into the `go.mod` + */ +async function writeGoMod({ + destDir, + goModPath, + packageName, +}: { + destDir: string; + goModPath?: string; + packageName: string; +}) { + let contents = `module ${packageName}`; + + if (goModPath) { + const goModRelPath = relative(destDir, dirname(goModPath)); + const goModContents = await readFile(goModPath, 'utf-8'); + + contents = goModContents + .replace(/^module\s+.+$/m, contents) + .replace( + /^(replace .+=>\s*)(.+)$/gm, + (orig, replaceStmt, replacePath) => { + if (replacePath.startsWith('.')) { + return replaceStmt + join(goModRelPath, replacePath); + } + return orig; + } + ); + + // get the module name, then add the 'replace' mapping if it doesn't + // already exist + const matches = goModContents.match(/module\s+(.+)/); + const moduleName = matches ? matches[1] : null; + if (moduleName) { + let relPath = normalize(goModRelPath); + if (!relPath.endsWith('/')) { + relPath += '/'; + } + + const requireRE = new RegExp(`require\\s+${moduleName}`); + const requireGroupRE = new RegExp( + `require\\s*\\(.*${moduleName}.*\\)`, + 's' + ); + if (!requireRE.test(contents) && !requireGroupRE.test(contents)) { + contents += `require ${moduleName} v0.0.0-unpublished\n`; + } + + const replaceRE = new RegExp(`replace.+=>\\s+${relPath}(\\s|$)`); + if (!replaceRE.test(contents)) { + contents += `replace ${moduleName} => ${relPath}\n`; + } + } + } + + const destGoModPath = join(destDir, 'go.mod'); + debug(`Writing ${destGoModPath}`); + // console.log(contents); + await writeFile(destGoModPath, contents, 'utf-8'); +} + +/** + * Attempts to find the `go.work` file. It will stop once it hits the + * `workPath`. + * @param goWorkDir The directory under the `wordPath` to start searching. + * @param workPath The project root to stop looking for the file. + * @returns The path to the `go.work` file or `undefined`. + */ +async function findGoWorkFile(goWorkDir: string, workPath: string) { + while (!(await pathExists(join(goWorkDir, 'go.work')))) { + if (goWorkDir === workPath) { + return; + } + goWorkDir = dirname(goWorkDir); + } + return join(goWorkDir, 'go.work'); +} + +/** + * For simple cases, a `go.work` file is not required. However when a Go + * program requires source files outside the work path, we need a `go.work` so + * Go can find the root of the project being built. + * @param destDir The destination directory to write the `go.work` file. + * @param workPath The path to the work directory. + * @param modulePath The path to the directory containing the `go.mod`. + */ +async function writeGoWork( + destDir: string, + workPath: string, + modulePath?: string +) { + const workspaces = new Set(['.']); + const goWorkPath = await findGoWorkFile(modulePath || workPath, workPath); + + if (goWorkPath) { + const contents = await readFile(goWorkPath, 'utf-8'); + const addPath = (path: string) => { + if (path) { + if (path.startsWith('.')) { + workspaces.add(relative(destDir, join(workPath, path))); + } else { + workspaces.add(path); + } + } + }; + + // find grouped paths + const multiRE = /use\s*\(([^)]+)/g; + let match = multiRE.exec(contents); + while (match) { + if (match[1]) { + for (const line of match[1].split(/\r?\n/)) { + addPath(line.trim()); + } + } + match = multiRE.exec(contents); + } + + // find single paths + const singleRE = /use\s+(?!\()(.+)/g; + match = singleRE.exec(contents); + while (match) { + addPath(match[1].trim()); + match = singleRE.exec(contents); + } + } else if (modulePath) { + workspaces.add(relative(destDir, modulePath)); + } + + const contents = `use (\n${Array.from(workspaces) + .map(w => ` ${w}\n`) + .join('')})\n`; + // console.log(contents); + await writeFile(join(destDir, 'go.work'), contents, 'utf-8'); } export async function startDevServer( @@ -669,27 +802,26 @@ export async function startDevServer( const tmpPackage = join(tmp, entrypointDir); await mkdirp(tmpPackage); - let goModAbsPathDir = ''; - if (await pathExists(join(workPath, 'go.mod'))) { - goModAbsPathDir = workPath; - } - const analyzedRaw = await getAnalyzedEntrypoint( - workPath, - entrypointWithExt, - goModAbsPathDir + const { goModPath } = await findGoModPath( + join(workPath, entrypointDir), + workPath ); - if (!analyzedRaw) { - throw new Error( - `Could not find an exported function in "${entrypointWithExt}" -Learn more: https://vercel.com/docs/runtimes#official-runtimes/go` - ); - } - const analyzed: Analyzed = JSON.parse(analyzedRaw); + const modulePath = goModPath ? dirname(goModPath) : undefined; + const analyzed = await getAnalyzedEntrypoint({ + entrypoint: entrypointWithExt, + modulePath, + workPath, + }); await Promise.all([ copyEntrypoint(entrypointWithExt, tmpPackage), copyDevServer(analyzed.functionName, tmpPackage), - goModAbsPathDir ? null : writeDefaultGoMod(tmp, analyzed.packageName), + writeGoMod({ + destDir: tmp, + goModPath, + packageName: analyzed.packageName, + }), + writeGoWork(tmp, workPath, modulePath), ]); const portFile = join( @@ -711,7 +843,7 @@ Learn more: https://vercel.com/docs/runtimes#official-runtimes/go` // build the dev server const go = await createGo({ - modulePath: goModAbsPathDir, + modulePath, opts: { cwd: tmp, env, @@ -815,7 +947,7 @@ export async function prepareCache({ // // On the next build, the local cache will be restored and `createGo()` will // use it unless the preferred Go version changed in the `go.mod`. - const goCacheDir = join(workPath, cacheDir); + const goCacheDir = join(workPath, localCacheDir); const stat = await lstat(goCacheDir); if (stat.isSymbolicLink()) { const goGlobalCacheDir = await readlink(goCacheDir); @@ -824,6 +956,6 @@ export async function prepareCache({ await move(goGlobalCacheDir, goCacheDir); } - const cache = await glob(`${cacheDir}/**`, workPath); + const cache = await glob(`${localCacheDir}/**`, workPath); return cache; } diff --git a/packages/go/test/fixtures/01-cowsay/vercel.json b/packages/go/test/fixtures/01-cowsay/vercel.json index 5eca89933..3590e8dc0 100644 --- a/packages/go/test/fixtures/01-cowsay/vercel.json +++ b/packages/go/test/fixtures/01-cowsay/vercel.json @@ -5,10 +5,10 @@ { "src": "subdirectory/index.go", "use": "@vercel/go" } ], "probes": [ - { "path": "/", "mustContain": "cow:go1.20.1:RANDOMNESS_PLACEHOLDER" }, + { "path": "/", "mustContain": "cow:go1.20.2:RANDOMNESS_PLACEHOLDER" }, { "path": "/subdirectory", - "mustContain": "subcow:go1.20.1:RANDOMNESS_PLACEHOLDER" + "mustContain": "subcow:go1.20.2:RANDOMNESS_PLACEHOLDER" } ] } diff --git a/packages/go/test/fixtures/02-parallel/api/go.mod b/packages/go/test/fixtures/02-parallel/api/go.mod index c945c59b1..38628d32e 100644 --- a/packages/go/test/fixtures/02-parallel/api/go.mod +++ b/packages/go/test/fixtures/02-parallel/api/go.mod @@ -1,3 +1,3 @@ module go-example -go 1.12 +go 1.20 diff --git a/packages/go/test/fixtures/12-go-mod-subs/sub-1/go.mod b/packages/go/test/fixtures/12-go-mod-subs/sub-1/go.mod index 1441fa9dd..a3dc4c66f 100644 --- a/packages/go/test/fixtures/12-go-mod-subs/sub-1/go.mod +++ b/packages/go/test/fixtures/12-go-mod-subs/sub-1/go.mod @@ -1,3 +1,3 @@ module sub-1 -go 1.12 +go 1.20 diff --git a/packages/go/test/fixtures/12-go-mod-subs/sub-2/go.mod b/packages/go/test/fixtures/12-go-mod-subs/sub-2/go.mod index bb36a70a4..1953fb6f6 100644 --- a/packages/go/test/fixtures/12-go-mod-subs/sub-2/go.mod +++ b/packages/go/test/fixtures/12-go-mod-subs/sub-2/go.mod @@ -1,3 +1,3 @@ module sub-2 -go 1.12 +go 1.20 diff --git a/packages/go/test/fixtures/14-go-mod-sub/other-folder/go.mod b/packages/go/test/fixtures/14-go-mod-sub/other-folder/go.mod index b7166a9e9..94abc5639 100644 --- a/packages/go/test/fixtures/14-go-mod-sub/other-folder/go.mod +++ b/packages/go/test/fixtures/14-go-mod-sub/other-folder/go.mod @@ -1,3 +1,3 @@ module other-folder -go 1.12 +go 1.20 diff --git a/packages/go/test/fixtures/16-custom-flag/probes.json b/packages/go/test/fixtures/16-custom-flag/probes.json new file mode 100644 index 000000000..057cb7094 --- /dev/null +++ b/packages/go/test/fixtures/16-custom-flag/probes.json @@ -0,0 +1,8 @@ +{ + "probes": [ + { + "path": "/", + "mustContain": "version:go1.14.15:first:RANDOMNESS_PLACEHOLDER" + } + ] +} diff --git a/packages/go/test/fixtures/16-custom-flag/vercel.json b/packages/go/test/fixtures/16-custom-flag/vercel.json index 2f014cbbc..44a47185b 100644 --- a/packages/go/test/fixtures/16-custom-flag/vercel.json +++ b/packages/go/test/fixtures/16-custom-flag/vercel.json @@ -1,11 +1,5 @@ { "version": 2, "builds": [{ "src": "index.go", "use": "@vercel/go" }], - "build": { "env": { "GO_BUILD_FLAGS": "-tags first -ldflags '-s -w'" } }, - "probes": [ - { - "path": "/", - "mustContain": "version:go1.14.15:first:RANDOMNESS_PLACEHOLDER" - } - ] + "build": { "env": { "GO_BUILD_FLAGS": "-tags first -ldflags '-s -w'" } } } diff --git a/packages/go/test/fixtures/17-go-mod-api/api/go.mod b/packages/go/test/fixtures/17-go-mod-api/api/go.mod index 120710f6a..f4a06225f 100644 --- a/packages/go/test/fixtures/17-go-mod-api/api/go.mod +++ b/packages/go/test/fixtures/17-go-mod-api/api/go.mod @@ -2,4 +2,10 @@ module github.com/vercel/does-not-exist go 1.13 -require github.com/dhruvbird/go-cowsay v0.0.0-20131019225157-6fd7bd0281c0 // indirect +require ( + github.com/dhruvbird/go-cowsay v0.0.0-20131019225157-6fd7bd0281c0 // indirect + + github.com/vercel/does-not-exist/api v0.0.0-unpublished +) + +replace github.com/vercel/does-not-exist/api => ./ diff --git a/packages/go/test/fixtures/17-go-mod-api/vercel.json b/packages/go/test/fixtures/17-go-mod-api/probes.json similarity index 90% rename from packages/go/test/fixtures/17-go-mod-api/vercel.json rename to packages/go/test/fixtures/17-go-mod-api/probes.json index da42fae47..816fa7667 100644 --- a/packages/go/test/fixtures/17-go-mod-api/vercel.json +++ b/packages/go/test/fixtures/17-go-mod-api/probes.json @@ -1,5 +1,4 @@ { - "version": 2, "probes": [ { "path": "/api/v1/routes/someroute", diff --git a/packages/go/test/fixtures/26-go-work-with-shared/api/go.mod b/packages/go/test/fixtures/26-go-work-with-shared/api/go.mod new file mode 100644 index 000000000..28f8032d4 --- /dev/null +++ b/packages/go/test/fixtures/26-go-work-with-shared/api/go.mod @@ -0,0 +1,3 @@ +module go-work-with-shared/api + +go 1.20 diff --git a/packages/go/test/fixtures/26-go-work-with-shared/api/index.go b/packages/go/test/fixtures/26-go-work-with-shared/api/index.go new file mode 100644 index 000000000..495cf883c --- /dev/null +++ b/packages/go/test/fixtures/26-go-work-with-shared/api/index.go @@ -0,0 +1,12 @@ +package handler + +import ( + "fmt" + "net/http" + "go-work-with-shared/mylib" +) + +// Handler function +func Handler(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, mylib.Say("hello")) +} diff --git a/packages/go/test/fixtures/26-go-work-with-shared/go.work b/packages/go/test/fixtures/26-go-work-with-shared/go.work new file mode 100644 index 000000000..3deb8aab1 --- /dev/null +++ b/packages/go/test/fixtures/26-go-work-with-shared/go.work @@ -0,0 +1,6 @@ +go 1.20 + +use ( + ./api + ./mylib +) diff --git a/packages/go/test/fixtures/26-go-work-with-shared/mylib/go.mod b/packages/go/test/fixtures/26-go-work-with-shared/mylib/go.mod new file mode 100644 index 000000000..e7010fef8 --- /dev/null +++ b/packages/go/test/fixtures/26-go-work-with-shared/mylib/go.mod @@ -0,0 +1,3 @@ +module go-work-with-shared/mylib + +go 1.20 diff --git a/packages/go/test/fixtures/26-go-work-with-shared/mylib/main.go b/packages/go/test/fixtures/26-go-work-with-shared/mylib/main.go new file mode 100644 index 000000000..1a975cf31 --- /dev/null +++ b/packages/go/test/fixtures/26-go-work-with-shared/mylib/main.go @@ -0,0 +1,9 @@ +package mylib + +import ( + "runtime" +) + +func Say(text string) string { + return text + ":" + runtime.Version() +} diff --git a/packages/go/test/fixtures/26-go-work-with-shared/probes.json b/packages/go/test/fixtures/26-go-work-with-shared/probes.json new file mode 100644 index 000000000..e2fc24c40 --- /dev/null +++ b/packages/go/test/fixtures/26-go-work-with-shared/probes.json @@ -0,0 +1,8 @@ +{ + "probes": [ + { + "path": "/api/index.go", + "mustContain": "hello:go1.20.2" + } + ] +}