mirror of
https://github.com/LukeHagar/vercel.git
synced 2025-12-10 04:22:12 +00:00
[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.
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
module go-work-with-shared/api
|
||||
|
||||
go 1.20
|
||||
@@ -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"))
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
go 1.20
|
||||
|
||||
use (
|
||||
./api/
|
||||
./mylib/
|
||||
)
|
||||
@@ -0,0 +1,3 @@
|
||||
module go-work-with-shared/mylib
|
||||
|
||||
go 1.20
|
||||
@@ -0,0 +1,9 @@
|
||||
package mylib
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
)
|
||||
|
||||
func Say(text string) string {
|
||||
return text + ":" + runtime.Version()
|
||||
}
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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}`);
|
||||
interface Analyzed {
|
||||
functionName: string;
|
||||
packageName: string;
|
||||
watch?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<Analyzed> {
|
||||
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');
|
||||
const go = await createGo({
|
||||
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(`Analyzing entrypoint ${filePath} with modulePath ${modulePath}`);
|
||||
const args = [`-modpath=${modulePath}`, filePath];
|
||||
const analyzed = await execa.stdout(bin, args);
|
||||
debug(`Analyzed entrypoint ${analyzed}`);
|
||||
return 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;
|
||||
}
|
||||
|
||||
class GoWrapper {
|
||||
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}`);
|
||||
}
|
||||
|
||||
@@ -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 (!goModPath && (await pathExists(join(workPath, 'vendor')))) {
|
||||
throw new Error('`go.mod` is required to use a `vendor` directory.');
|
||||
}
|
||||
|
||||
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 analyzed = await getAnalyzedEntrypoint({
|
||||
entrypoint,
|
||||
modulePath: goModPath ? dirname(goModPath) : undefined,
|
||||
workPath,
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
// 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 input = entrypointDirname;
|
||||
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, input);
|
||||
for (const [assetName, asset] of Object.entries(fsFiles)) {
|
||||
includedFiles[assetName] = asset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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,120 +199,183 @@ 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) {
|
||||
const outDir = await getWriteableDirectory();
|
||||
const buildOptions: BuildHandlerOptions = {
|
||||
downloadPath,
|
||||
entrypoint,
|
||||
entrypointAbsolute,
|
||||
entrypointDirname,
|
||||
go,
|
||||
goModPath,
|
||||
handlerFunctionName,
|
||||
isGoModInRootDir,
|
||||
outDir,
|
||||
packageName,
|
||||
undo,
|
||||
};
|
||||
|
||||
if (packageName === 'main') {
|
||||
await buildHandlerAsPackageMain(buildOptions);
|
||||
} else {
|
||||
await buildHandlerWithGoMod(buildOptions);
|
||||
}
|
||||
|
||||
const lambda = new Lambda({
|
||||
files: { ...(await glob('**', outDir)), ...includedFiles },
|
||||
handler: HANDLER_FILENAME,
|
||||
runtime: 'go1.x',
|
||||
supportsWrapper: true,
|
||||
environment: {},
|
||||
});
|
||||
|
||||
return {
|
||||
output: lambda,
|
||||
};
|
||||
} catch (error) {
|
||||
debug(`Go Builder Error: ${error}`);
|
||||
|
||||
throw error;
|
||||
} finally {
|
||||
try {
|
||||
const defaultGoModContent = `module ${packageName}`;
|
||||
await cleanupFileSystem(undo);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
console.error(`Build cleanup failed: ${error.message}`);
|
||||
}
|
||||
debug('Cleanup Error: ' + error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await writeFile(
|
||||
join(entrypointDirname, 'go.mod'),
|
||||
defaultGoModContent
|
||||
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<void> {
|
||||
debug(
|
||||
`Building Go handler as package "${packageName}" (with${
|
||||
goModPath ? '' : 'out'
|
||||
} go.mod)`
|
||||
);
|
||||
|
||||
undoFileActions.push({
|
||||
to: undefined, // delete
|
||||
from: join(entrypointDirname, '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
|
||||
undoFileActions.push({
|
||||
undo.fileActions.push({
|
||||
to: undefined, // delete
|
||||
from: join(entrypointDirname, 'go.sum'),
|
||||
from: goSumPath,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(`Failed to create default go.mod for ${packageName}`);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
const modMainGoContents = await readFile(
|
||||
join(__dirname, 'main.go'),
|
||||
'utf8'
|
||||
);
|
||||
|
||||
const entrypointArr = entrypoint.split(posix.sep);
|
||||
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('/')}`;
|
||||
// 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 {
|
||||
goPackageName = `${usrModName}/${packageName}`;
|
||||
}
|
||||
debug(`[entrypoint] Write main file to ${entrypointDirname}`);
|
||||
mainGoFile = join(entrypointDirname, MAIN_GO_FILENAME);
|
||||
}
|
||||
|
||||
const mainModGoContents = modMainGoContents
|
||||
.replace('__VC_HANDLER_PACKAGE_NAME', goPackageName)
|
||||
.replace('__VC_HANDLER_FUNC_NAME', goFuncName);
|
||||
await Promise.all([
|
||||
writeEntrypoint(mainGoFile, goPackageName, goFuncName),
|
||||
writeGoMod({
|
||||
destDir: goModDirname ? goModDirname : entrypointDirname,
|
||||
goModPath,
|
||||
packageName,
|
||||
}),
|
||||
]);
|
||||
|
||||
if (isGoModExist && isGoModInRootDir) {
|
||||
debug('[mod-root] Write main file to ' + downloadPath);
|
||||
await writeFile(join(downloadPath, mainGoFileName), mainModGoContents);
|
||||
|
||||
undoFileActions.push({
|
||||
undo.fileActions.push({
|
||||
to: undefined, // delete
|
||||
from: join(downloadPath, mainGoFileName),
|
||||
from: mainGoFile,
|
||||
});
|
||||
} 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 {
|
||||
@@ -396,17 +391,17 @@ export async function build({
|
||||
);
|
||||
}
|
||||
|
||||
if (dirname(entrypointAbsolute) === goModPath || !isGoModExist) {
|
||||
if (!goModPath || dirname(entrypointAbsolute) === dirname(goModPath)) {
|
||||
debug(
|
||||
`moving entrypoint "${entrypointAbsolute}" to "${finalDestination}"`
|
||||
);
|
||||
|
||||
await move(entrypointAbsolute, finalDestination);
|
||||
undoFileActions.push({
|
||||
undo.fileActions.push({
|
||||
to: entrypointAbsolute,
|
||||
from: finalDestination,
|
||||
});
|
||||
undoDirectoryCreation.push(dirname(finalDestination));
|
||||
undo.directoryCreation.push(dirname(finalDestination));
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to move entry to package folder');
|
||||
@@ -414,10 +409,10 @@ export async function build({
|
||||
}
|
||||
|
||||
let baseGoModPath = '';
|
||||
if (isGoModExist && isGoModInRootDir) {
|
||||
if (goModPath && isGoModInRootDir) {
|
||||
baseGoModPath = downloadPath;
|
||||
} else if (isGoModExist && !isGoModInRootDir) {
|
||||
baseGoModPath = goModPath;
|
||||
} else if (goModPath && !isGoModInRootDir) {
|
||||
baseGoModPath = dirname(goModPath);
|
||||
} else {
|
||||
baseGoModPath = entrypointDirname;
|
||||
}
|
||||
@@ -432,34 +427,42 @@ export async function build({
|
||||
}
|
||||
|
||||
debug('Running `go build`...');
|
||||
const destPath = join(outDir, handlerFileName);
|
||||
const destPath = join(outDir, HANDLER_FILENAME);
|
||||
|
||||
try {
|
||||
const src = [join(baseGoModPath, mainGoFileName)];
|
||||
const src = [join(baseGoModPath, MAIN_GO_FILENAME)];
|
||||
|
||||
await go.build(src, destPath);
|
||||
} catch (err) {
|
||||
console.error('failed to `go build`');
|
||||
throw err;
|
||||
}
|
||||
} 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({
|
||||
/**
|
||||
* 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<void> {
|
||||
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, mainGoFileName),
|
||||
from: join(entrypointDirname, MAIN_GO_FILENAME),
|
||||
});
|
||||
|
||||
// `go get` will look at `*.go` (note we set `cwd`), parse the `import`s
|
||||
@@ -473,10 +476,10 @@ export async function build({
|
||||
}
|
||||
|
||||
debug('Running `go build`...');
|
||||
const destPath = join(outDir, handlerFileName);
|
||||
const destPath = join(outDir, HANDLER_FILENAME);
|
||||
try {
|
||||
const src = [
|
||||
join(entrypointDirname, mainGoFileName),
|
||||
join(entrypointDirname, MAIN_GO_FILENAME),
|
||||
entrypointAbsolute,
|
||||
].map(file => normalize(file));
|
||||
await go.build(src, destPath);
|
||||
@@ -484,37 +487,6 @@ export async function build({
|
||||
console.error('failed to `go build`');
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
const lambda = await createLambda({
|
||||
files: { ...(await glob('**', outDir)), ...includedFiles },
|
||||
handler: handlerFileName,
|
||||
runtime: 'go1.x',
|
||||
supportsWrapper: true,
|
||||
environment: {},
|
||||
});
|
||||
|
||||
return {
|
||||
output: lambda,
|
||||
};
|
||||
} catch (error) {
|
||||
debug('Go Builder Error: ' + error);
|
||||
|
||||
throw error;
|
||||
} finally {
|
||||
try {
|
||||
await cleanupFileSystem(
|
||||
undoFileActions,
|
||||
undoDirectoryCreation,
|
||||
undoFunctionRenames
|
||||
);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
console.error(`Build cleanup failed: ${error.message}`);
|
||||
}
|
||||
debug('Cleanup Error: ' + error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function renameHandlerFunction(fsPath: string, from: string, to: string) {
|
||||
@@ -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<string> {
|
||||
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(
|
||||
const { goModPath } = await findGoModPath(
|
||||
join(workPath, entrypointDir),
|
||||
workPath
|
||||
);
|
||||
const modulePath = goModPath ? dirname(goModPath) : undefined;
|
||||
const analyzed = await getAnalyzedEntrypoint({
|
||||
entrypoint: entrypointWithExt,
|
||||
modulePath,
|
||||
workPath,
|
||||
entrypointWithExt,
|
||||
goModAbsPathDir
|
||||
);
|
||||
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);
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
module go-example
|
||||
|
||||
go 1.12
|
||||
go 1.20
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
module sub-1
|
||||
|
||||
go 1.12
|
||||
go 1.20
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
module sub-2
|
||||
|
||||
go 1.12
|
||||
go 1.20
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
module other-folder
|
||||
|
||||
go 1.12
|
||||
go 1.20
|
||||
|
||||
8
packages/go/test/fixtures/16-custom-flag/probes.json
vendored
Normal file
8
packages/go/test/fixtures/16-custom-flag/probes.json
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"probes": [
|
||||
{
|
||||
"path": "/",
|
||||
"mustContain": "version:go1.14.15:first:RANDOMNESS_PLACEHOLDER"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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'" } }
|
||||
}
|
||||
|
||||
@@ -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 => ./
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"version": 2,
|
||||
"probes": [
|
||||
{
|
||||
"path": "/api/v1/routes/someroute",
|
||||
3
packages/go/test/fixtures/26-go-work-with-shared/api/go.mod
vendored
Normal file
3
packages/go/test/fixtures/26-go-work-with-shared/api/go.mod
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
module go-work-with-shared/api
|
||||
|
||||
go 1.20
|
||||
12
packages/go/test/fixtures/26-go-work-with-shared/api/index.go
vendored
Normal file
12
packages/go/test/fixtures/26-go-work-with-shared/api/index.go
vendored
Normal file
@@ -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"))
|
||||
}
|
||||
6
packages/go/test/fixtures/26-go-work-with-shared/go.work
vendored
Normal file
6
packages/go/test/fixtures/26-go-work-with-shared/go.work
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
go 1.20
|
||||
|
||||
use (
|
||||
./api
|
||||
./mylib
|
||||
)
|
||||
3
packages/go/test/fixtures/26-go-work-with-shared/mylib/go.mod
vendored
Normal file
3
packages/go/test/fixtures/26-go-work-with-shared/mylib/go.mod
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
module go-work-with-shared/mylib
|
||||
|
||||
go 1.20
|
||||
9
packages/go/test/fixtures/26-go-work-with-shared/mylib/main.go
vendored
Normal file
9
packages/go/test/fixtures/26-go-work-with-shared/mylib/main.go
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
package mylib
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
)
|
||||
|
||||
func Say(text string) string {
|
||||
return text + ":" + runtime.Version()
|
||||
}
|
||||
8
packages/go/test/fixtures/26-go-work-with-shared/probes.json
vendored
Normal file
8
packages/go/test/fixtures/26-go-work-with-shared/probes.json
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"probes": [
|
||||
{
|
||||
"path": "/api/index.go",
|
||||
"mustContain": "hello:go1.20.2"
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user