mirror of
https://github.com/LukeHagar/vercel.git
synced 2025-12-10 12:57:47 +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(
|
test(
|
||||||
'[vercel dev] Should set the `ts-node` "target" to match Node.js version',
|
'[vercel dev] Should set the `ts-node` "target" to match Node.js version',
|
||||||
testFixtureStdio('node-ts-node-target', async (testPath: any) => {
|
testFixtureStdio('node-ts-node-target', async (testPath: any) => {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
remove,
|
remove,
|
||||||
symlink,
|
symlink,
|
||||||
} from 'fs-extra';
|
} from 'fs-extra';
|
||||||
import { join, delimiter, dirname } from 'path';
|
import { delimiter, dirname, join } from 'path';
|
||||||
import stringArgv from 'string-argv';
|
import stringArgv from 'string-argv';
|
||||||
import { cloneEnv, debug } from '@vercel/build-utils';
|
import { cloneEnv, debug } from '@vercel/build-utils';
|
||||||
import { pipeline } from 'stream';
|
import { pipeline } from 'stream';
|
||||||
@@ -22,7 +22,7 @@ import type { Env } from '@vercel/build-utils';
|
|||||||
const streamPipeline = promisify(pipeline);
|
const streamPipeline = promisify(pipeline);
|
||||||
|
|
||||||
const versionMap = new Map([
|
const versionMap = new Map([
|
||||||
['1.20', '1.20.1'],
|
['1.20', '1.20.2'],
|
||||||
['1.19', '1.19.6'],
|
['1.19', '1.19.6'],
|
||||||
['1.18', '1.18.10'],
|
['1.18', '1.18.10'],
|
||||||
['1.17', '1.17.13'],
|
['1.17', '1.17.13'],
|
||||||
@@ -36,17 +36,32 @@ const archMap = new Map([
|
|||||||
['x86', '386'],
|
['x86', '386'],
|
||||||
]);
|
]);
|
||||||
const platformMap = new Map([['win32', 'windows']]);
|
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_FLAGS = process.platform === 'win32' ? [] : ['-ldflags', '-s -w'];
|
||||||
const GO_MIN_VERSION = 13;
|
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) {
|
function getGoUrl(version: string) {
|
||||||
const { arch, platform } = process;
|
const { arch, platform } = process;
|
||||||
const goArch = getArch(arch);
|
|
||||||
const goPlatform = getPlatform(platform);
|
|
||||||
const ext = platform === 'win32' ? 'zip' : 'tar.gz';
|
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}`;
|
const filename = `go${version}.${goPlatform}-${goArch}.${ext}`;
|
||||||
return {
|
return {
|
||||||
filename,
|
filename,
|
||||||
@@ -61,32 +76,91 @@ export const goGlobalCachePath = join(
|
|||||||
|
|
||||||
export const OUT_EXTENSION = process.platform === 'win32' ? '.exe' : '';
|
export const OUT_EXTENSION = process.platform === 'win32' ? '.exe' : '';
|
||||||
|
|
||||||
export async function getAnalyzedEntrypoint(
|
interface Analyzed {
|
||||||
workPath: string,
|
functionName: string;
|
||||||
filePath: string,
|
packageName: string;
|
||||||
modulePath: string
|
watch?: boolean;
|
||||||
) {
|
}
|
||||||
const bin = join(__dirname, `analyze${OUT_EXTENSION}`);
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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);
|
const isAnalyzeExist = await pathExists(bin);
|
||||||
if (!isAnalyzeExist) {
|
if (!isAnalyzeExist) {
|
||||||
debug(`Building analyze bin: ${bin}`);
|
debug(`Building analyze bin: ${bin}`);
|
||||||
const src = join(__dirname, 'util', 'analyze.go');
|
const src = join(__dirname, 'util', 'analyze.go');
|
||||||
const go = await createGo({
|
let go;
|
||||||
|
const createOpts = {
|
||||||
modulePath,
|
modulePath,
|
||||||
|
opts: { cwd: __dirname },
|
||||||
workPath,
|
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);
|
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}`);
|
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 env: Env;
|
||||||
private opts: execa.Options;
|
private opts: execa.Options;
|
||||||
|
|
||||||
@@ -101,10 +175,11 @@ class GoWrapper {
|
|||||||
private execute(...args: string[]) {
|
private execute(...args: string[]) {
|
||||||
const { opts, env } = this;
|
const { opts, env } = this;
|
||||||
debug(
|
debug(
|
||||||
`Exec: go ${args
|
`Exec: go ${args.map(a => (a.includes(' ') ? `"${a}"` : a)).join(' ')}`
|
||||||
.map(a => (a.includes(' ') ? `"${a}"` : a))
|
|
||||||
.join(' ')} CWD=${opts.cwd}`
|
|
||||||
);
|
);
|
||||||
|
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 });
|
return execa('go', args, { stdio: 'inherit', ...opts, env });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,9 +202,8 @@ class GoWrapper {
|
|||||||
debug(`Building optimized 'go' binary ${src} -> ${dest}`);
|
debug(`Building optimized 'go' binary ${src} -> ${dest}`);
|
||||||
const sources = Array.isArray(src) ? src : [src];
|
const sources = Array.isArray(src) ? src : [src];
|
||||||
|
|
||||||
const flags = process.env.GO_BUILD_FLAGS
|
const envGoBuildFlags = (this.env || this.opts.env).GO_BUILD_FLAGS;
|
||||||
? stringArgv(process.env.GO_BUILD_FLAGS)
|
const flags = envGoBuildFlags ? stringArgv(envGoBuildFlags) : GO_FLAGS;
|
||||||
: GO_FLAGS;
|
|
||||||
|
|
||||||
return this.execute('build', ...flags, '-o', dest, ...sources);
|
return this.execute('build', ...flags, '-o', dest, ...sources);
|
||||||
}
|
}
|
||||||
@@ -186,7 +260,7 @@ export async function createGo({
|
|||||||
goGlobalCachePath,
|
goGlobalCachePath,
|
||||||
`${goSelectedVersion}_${platform}_${process.arch}`
|
`${goSelectedVersion}_${platform}_${process.arch}`
|
||||||
);
|
);
|
||||||
const goCacheDir = join(workPath, cacheDir);
|
const goCacheDir = join(workPath, localCacheDir);
|
||||||
|
|
||||||
if (goPreferredVersion) {
|
if (goPreferredVersion) {
|
||||||
debug(`Preferred go version ${goPreferredVersion} (from go.mod)`);
|
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`);
|
debug(`Found go ${version} in ${label}, but version is unsupported`);
|
||||||
}
|
}
|
||||||
if (version === goSelectedVersion || short === goSelectedVersion) {
|
if (version === goSelectedVersion || short === goSelectedVersion) {
|
||||||
console.log(`Selected go ${version} (from ${label})`);
|
debug(`Selected go ${version} (from ${label})`);
|
||||||
|
|
||||||
await setGoEnv(goDir);
|
await setGoEnv(goDir);
|
||||||
return new GoWrapper(env, opts);
|
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.
|
* 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}`);
|
const full = versionMap.get(`${major}.${minor}`);
|
||||||
if (major === 1 && minor >= GO_MIN_VERSION && full) {
|
if (major === 1 && minor >= GO_MIN_VERSION && full) {
|
||||||
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 {
|
} else {
|
||||||
console.log(`Warning: Unknown Go version in ${file}`);
|
console.log(`Warning: Unknown Go version in ${file}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { homedir, tmpdir } from 'os';
|
|||||||
import { spawn } from 'child_process';
|
import { spawn } from 'child_process';
|
||||||
import { Readable } from 'stream';
|
import { Readable } from 'stream';
|
||||||
import once from '@tootallnate/once';
|
import once from '@tootallnate/once';
|
||||||
import { join, dirname, basename, normalize, posix, sep } from 'path';
|
import { basename, dirname, join, normalize, posix, relative } from 'path';
|
||||||
import {
|
import {
|
||||||
readFile,
|
readFile,
|
||||||
writeFile,
|
writeFile,
|
||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
rmdir,
|
rmdir,
|
||||||
readdir,
|
readdir,
|
||||||
unlink,
|
unlink,
|
||||||
|
copy,
|
||||||
} from 'fs-extra';
|
} from 'fs-extra';
|
||||||
import {
|
import {
|
||||||
BuildOptions,
|
BuildOptions,
|
||||||
@@ -26,7 +27,7 @@ import {
|
|||||||
StartDevServerResult,
|
StartDevServerResult,
|
||||||
glob,
|
glob,
|
||||||
download,
|
download,
|
||||||
createLambda,
|
Lambda,
|
||||||
getWriteableDirectory,
|
getWriteableDirectory,
|
||||||
shouldServe,
|
shouldServe,
|
||||||
debug,
|
debug,
|
||||||
@@ -36,21 +37,20 @@ import {
|
|||||||
const TMP = tmpdir();
|
const TMP = tmpdir();
|
||||||
|
|
||||||
import {
|
import {
|
||||||
cacheDir,
|
localCacheDir,
|
||||||
createGo,
|
createGo,
|
||||||
getAnalyzedEntrypoint,
|
getAnalyzedEntrypoint,
|
||||||
|
GoWrapper,
|
||||||
OUT_EXTENSION,
|
OUT_EXTENSION,
|
||||||
} from './go-helpers';
|
} from './go-helpers';
|
||||||
|
|
||||||
const handlerFileName = `handler${OUT_EXTENSION}`;
|
|
||||||
|
|
||||||
export { shouldServe };
|
export { shouldServe };
|
||||||
|
|
||||||
interface Analyzed {
|
// in order to allow the user to have `main.go`,
|
||||||
functionName: string;
|
// we need our `main.go` to be called something else
|
||||||
packageName: string;
|
const MAIN_GO_FILENAME = 'main__vc__go__.go';
|
||||||
watch?: boolean;
|
|
||||||
}
|
const HANDLER_FILENAME = `handler${OUT_EXTENSION}`;
|
||||||
|
|
||||||
interface PortInfo {
|
interface PortInfo {
|
||||||
port: number;
|
port: number;
|
||||||
@@ -100,6 +100,12 @@ type UndoFunctionRename = {
|
|||||||
to: string;
|
to: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type UndoActions = {
|
||||||
|
fileActions: UndoFileAction[];
|
||||||
|
directoryCreation: string[];
|
||||||
|
functionRenames: UndoFunctionRename[];
|
||||||
|
};
|
||||||
|
|
||||||
export const version = 3;
|
export const version = 3;
|
||||||
|
|
||||||
export async function build({
|
export async function build({
|
||||||
@@ -117,17 +123,24 @@ export async function build({
|
|||||||
// keep track of file system actions we need to undo
|
// keep track of file system actions we need to undo
|
||||||
// the keys "from" and "to" refer to what needs to be done
|
// the keys "from" and "to" refer to what needs to be done
|
||||||
// in order to undo the action, not what the original action was
|
// in order to undo the action, not what the original action was
|
||||||
const undoFileActions: UndoFileAction[] = [];
|
const undo: UndoActions = {
|
||||||
const undoDirectoryCreation: string[] = [];
|
fileActions: [],
|
||||||
const undoFunctionRenames: UndoFunctionRename[] = [];
|
directoryCreation: [],
|
||||||
|
functionRenames: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const env = cloneEnv(process.env, meta.env, {
|
||||||
|
GOARCH: 'amd64',
|
||||||
|
GOOS: 'linux',
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (process.env.GIT_CREDENTIALS) {
|
if (env.GIT_CREDENTIALS) {
|
||||||
debug('Initialize 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.
|
console.log(`\nManually assigning 'GO111MODULE' is not recommended.
|
||||||
|
|
||||||
By default:
|
By default:
|
||||||
@@ -143,7 +156,7 @@ export async function build({
|
|||||||
const renamedEntrypoint = getRenamedEntrypoint(entrypoint);
|
const renamedEntrypoint = getRenamedEntrypoint(entrypoint);
|
||||||
if (renamedEntrypoint) {
|
if (renamedEntrypoint) {
|
||||||
await move(join(workPath, entrypoint), join(workPath, renamedEntrypoint));
|
await move(join(workPath, entrypoint), join(workPath, renamedEntrypoint));
|
||||||
undoFileActions.push({
|
undo.fileActions.push({
|
||||||
to: join(workPath, entrypoint),
|
to: join(workPath, entrypoint),
|
||||||
from: join(workPath, renamedEntrypoint),
|
from: join(workPath, renamedEntrypoint),
|
||||||
});
|
});
|
||||||
@@ -151,113 +164,32 @@ export async function build({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const entrypointAbsolute = join(workPath, entrypoint);
|
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);
|
const entrypointDirname = dirname(entrypointAbsolute);
|
||||||
let isGoModExist = false;
|
|
||||||
let goModPath = '';
|
|
||||||
let isGoModInRootDir = false;
|
|
||||||
|
|
||||||
const modFileRefs = await glob('**/*.mod', workPath);
|
const { goModPath, isGoModInRootDir } = await findGoModPath(
|
||||||
const modFiles = Object.keys(modFileRefs);
|
entrypointDirname,
|
||||||
|
workPath
|
||||||
|
);
|
||||||
|
|
||||||
for (const file of modFiles) {
|
if (!goModPath && (await pathExists(join(workPath, 'vendor')))) {
|
||||||
const fileDirname = dirname(file);
|
throw new Error('`go.mod` is required to use a `vendor` directory.');
|
||||||
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') {
|
const analyzed = await getAnalyzedEntrypoint({
|
||||||
// We didn't find `/go.mod` but we found `/api/go.mod` so move it to the root
|
entrypoint,
|
||||||
isGoModExist = true;
|
modulePath: goModPath ? dirname(goModPath) : undefined,
|
||||||
isGoModInRootDir = true;
|
workPath,
|
||||||
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');
|
// check if package name other than main
|
||||||
const newSumPath = join(dirname(newFsPath), 'go.sum');
|
// using `go.mod` way building the handler
|
||||||
if (await pathExists(oldSumPath)) {
|
const packageName = analyzed.packageName;
|
||||||
debug(`Moving api/go.sum to root: ${oldSumPath} to ${newSumPath}`);
|
if (goModPath && packageName === 'main') {
|
||||||
await move(oldSumPath, newSumPath);
|
throw new Error('Please change `package main` to `package handler`');
|
||||||
undoFileActions.push({
|
|
||||||
to: oldSumPath,
|
|
||||||
from: newSumPath,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const input = entrypointDirname;
|
// rename the Go handler function name in the original entrypoint file
|
||||||
const includedFiles: Files = {};
|
const originalFunctionName = analyzed.functionName;
|
||||||
|
|
||||||
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;
|
|
||||||
const handlerFunctionName = getNewHandlerFunctionName(
|
const handlerFunctionName = getNewHandlerFunctionName(
|
||||||
originalFunctionName,
|
originalFunctionName,
|
||||||
entrypoint
|
entrypoint
|
||||||
@@ -267,120 +199,183 @@ export async function build({
|
|||||||
originalFunctionName,
|
originalFunctionName,
|
||||||
handlerFunctionName
|
handlerFunctionName
|
||||||
);
|
);
|
||||||
undoFunctionRenames.push({
|
undo.functionRenames.push({
|
||||||
fsPath: originalEntrypointAbsolute,
|
fsPath: originalEntrypointAbsolute,
|
||||||
from: handlerFunctionName,
|
from: handlerFunctionName,
|
||||||
to: originalFunctionName,
|
to: originalFunctionName,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!isGoModExist) {
|
const includedFiles: Files = {};
|
||||||
if (await pathExists(join(workPath, 'vendor'))) {
|
if (config && config.includeFiles) {
|
||||||
throw new Error('`go.mod` is required to use a `vendor` directory.');
|
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
|
const modulePath = goModPath ? dirname(goModPath) : undefined;
|
||||||
// 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 go = await createGo({
|
const go = await createGo({
|
||||||
modulePath: goModPath,
|
modulePath,
|
||||||
opts: {
|
opts: {
|
||||||
cwd: entrypointDirname,
|
cwd: entrypointDirname,
|
||||||
env: {
|
env,
|
||||||
GOARCH: 'amd64',
|
|
||||||
GOOS: 'linux',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
workPath,
|
workPath,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (packageName !== 'main') {
|
const outDir = await getWriteableDirectory();
|
||||||
if (!isGoModExist) {
|
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 {
|
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(
|
type BuildHandlerOptions = {
|
||||||
join(entrypointDirname, 'go.mod'),
|
downloadPath: string;
|
||||||
defaultGoModContent
|
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({
|
let goModDirname: string | undefined;
|
||||||
to: undefined, // delete
|
|
||||||
from: join(entrypointDirname, 'go.mod'),
|
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
|
// remove the `go.sum` file that will be generated as well
|
||||||
undoFileActions.push({
|
undo.fileActions.push({
|
||||||
to: undefined, // delete
|
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(
|
const entrypointArr = entrypoint.split(posix.sep);
|
||||||
join(__dirname, 'main.go'),
|
|
||||||
'utf8'
|
|
||||||
);
|
|
||||||
|
|
||||||
let goPackageName = `${packageName}/${packageName}`;
|
let goPackageName = `${packageName}/${packageName}`;
|
||||||
const goFuncName = `${packageName}.${handlerFunctionName}`;
|
const goFuncName = `${packageName}.${handlerFunctionName}`;
|
||||||
|
|
||||||
if (isGoModExist) {
|
// if we have a go.mod, determine the relative path of the entrypoint to the
|
||||||
const goModContents = await readFile(join(goModPath, 'go.mod'), 'utf8');
|
// go.mod directory and use that for the import package name in main.go
|
||||||
const usrModName = goModContents.split('\n')[0].split(' ')[1];
|
const relPackagePath = goModDirname
|
||||||
if (entrypointArr.length > 1 && isGoModInRootDir) {
|
? posix.relative(goModDirname, entrypointDirname)
|
||||||
const cleanPackagePath = [...entrypointArr];
|
: '';
|
||||||
cleanPackagePath.pop();
|
if (relPackagePath) {
|
||||||
goPackageName = `${usrModName}/${cleanPackagePath.join('/')}`;
|
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 {
|
} else {
|
||||||
goPackageName = `${usrModName}/${packageName}`;
|
debug(`[entrypoint] Write main file to ${entrypointDirname}`);
|
||||||
}
|
mainGoFile = join(entrypointDirname, MAIN_GO_FILENAME);
|
||||||
}
|
}
|
||||||
|
|
||||||
const mainModGoContents = modMainGoContents
|
await Promise.all([
|
||||||
.replace('__VC_HANDLER_PACKAGE_NAME', goPackageName)
|
writeEntrypoint(mainGoFile, goPackageName, goFuncName),
|
||||||
.replace('__VC_HANDLER_FUNC_NAME', goFuncName);
|
writeGoMod({
|
||||||
|
destDir: goModDirname ? goModDirname : entrypointDirname,
|
||||||
|
goModPath,
|
||||||
|
packageName,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
if (isGoModExist && isGoModInRootDir) {
|
undo.fileActions.push({
|
||||||
debug('[mod-root] Write main file to ' + downloadPath);
|
|
||||||
await writeFile(join(downloadPath, mainGoFileName), mainModGoContents);
|
|
||||||
|
|
||||||
undoFileActions.push({
|
|
||||||
to: undefined, // delete
|
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
|
// move user go file to folder
|
||||||
try {
|
try {
|
||||||
@@ -396,17 +391,17 @@ export async function build({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dirname(entrypointAbsolute) === goModPath || !isGoModExist) {
|
if (!goModPath || dirname(entrypointAbsolute) === dirname(goModPath)) {
|
||||||
debug(
|
debug(
|
||||||
`moving entrypoint "${entrypointAbsolute}" to "${finalDestination}"`
|
`moving entrypoint "${entrypointAbsolute}" to "${finalDestination}"`
|
||||||
);
|
);
|
||||||
|
|
||||||
await move(entrypointAbsolute, finalDestination);
|
await move(entrypointAbsolute, finalDestination);
|
||||||
undoFileActions.push({
|
undo.fileActions.push({
|
||||||
to: entrypointAbsolute,
|
to: entrypointAbsolute,
|
||||||
from: finalDestination,
|
from: finalDestination,
|
||||||
});
|
});
|
||||||
undoDirectoryCreation.push(dirname(finalDestination));
|
undo.directoryCreation.push(dirname(finalDestination));
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to move entry to package folder');
|
console.error('Failed to move entry to package folder');
|
||||||
@@ -414,10 +409,10 @@ export async function build({
|
|||||||
}
|
}
|
||||||
|
|
||||||
let baseGoModPath = '';
|
let baseGoModPath = '';
|
||||||
if (isGoModExist && isGoModInRootDir) {
|
if (goModPath && isGoModInRootDir) {
|
||||||
baseGoModPath = downloadPath;
|
baseGoModPath = downloadPath;
|
||||||
} else if (isGoModExist && !isGoModInRootDir) {
|
} else if (goModPath && !isGoModInRootDir) {
|
||||||
baseGoModPath = goModPath;
|
baseGoModPath = dirname(goModPath);
|
||||||
} else {
|
} else {
|
||||||
baseGoModPath = entrypointDirname;
|
baseGoModPath = entrypointDirname;
|
||||||
}
|
}
|
||||||
@@ -432,34 +427,42 @@ export async function build({
|
|||||||
}
|
}
|
||||||
|
|
||||||
debug('Running `go build`...');
|
debug('Running `go build`...');
|
||||||
const destPath = join(outDir, handlerFileName);
|
const destPath = join(outDir, HANDLER_FILENAME);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const src = [join(baseGoModPath, mainGoFileName)];
|
const src = [join(baseGoModPath, MAIN_GO_FILENAME)];
|
||||||
|
|
||||||
await go.build(src, destPath);
|
await go.build(src, destPath);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('failed to `go build`');
|
console.error('failed to `go build`');
|
||||||
throw err;
|
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
|
* Builds the wrapped Go function using the legacy mode where package name is
|
||||||
await writeFile(join(entrypointDirname, mainGoFileName), mainGoContents);
|
* `"main"` and we need `main.go` in the same dir as the entrypoint, otherwise
|
||||||
undoFileActions.push({
|
* `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
|
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
|
// `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`...');
|
debug('Running `go build`...');
|
||||||
const destPath = join(outDir, handlerFileName);
|
const destPath = join(outDir, HANDLER_FILENAME);
|
||||||
try {
|
try {
|
||||||
const src = [
|
const src = [
|
||||||
join(entrypointDirname, mainGoFileName),
|
join(entrypointDirname, MAIN_GO_FILENAME),
|
||||||
entrypointAbsolute,
|
entrypointAbsolute,
|
||||||
].map(file => normalize(file));
|
].map(file => normalize(file));
|
||||||
await go.build(src, destPath);
|
await go.build(src, destPath);
|
||||||
@@ -484,37 +487,6 @@ export async function build({
|
|||||||
console.error('failed to `go build`');
|
console.error('failed to `go build`');
|
||||||
throw err;
|
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) {
|
async function renameHandlerFunction(fsPath: string, from: string, to: string) {
|
||||||
@@ -560,17 +532,20 @@ export function getNewHandlerFunctionName(
|
|||||||
return newHandlerName;
|
return newHandlerName;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function cleanupFileSystem(
|
/**
|
||||||
undoFileActions: UndoFileAction[],
|
* Remove any temporary files, directories, and file changes.
|
||||||
undoDirectoryCreation: string[],
|
*/
|
||||||
undoFunctionRenames: UndoFunctionRename[]
|
async function cleanupFileSystem({
|
||||||
) {
|
fileActions,
|
||||||
|
directoryCreation,
|
||||||
|
functionRenames,
|
||||||
|
}: UndoActions) {
|
||||||
// we have to undo the actions in reverse order in cases
|
// we have to undo the actions in reverse order in cases
|
||||||
// where one file was moved multiple times, which happens
|
// where one file was moved multiple times, which happens
|
||||||
// using files that start with brackets
|
// using files that start with brackets
|
||||||
for (const action of undoFileActions.reverse()) {
|
for (const action of fileActions.reverse()) {
|
||||||
if (action.to) {
|
if (action.to) {
|
||||||
await move(action.from, action.to);
|
await move(action.from, action.to, { overwrite: true });
|
||||||
} else {
|
} else {
|
||||||
await remove(action.from);
|
await remove(action.from);
|
||||||
}
|
}
|
||||||
@@ -578,11 +553,11 @@ async function cleanupFileSystem(
|
|||||||
|
|
||||||
// after files are moved back, we can undo function renames
|
// after files are moved back, we can undo function renames
|
||||||
// these reference the original file location
|
// these reference the original file location
|
||||||
for (const rename of undoFunctionRenames) {
|
for (const rename of functionRenames) {
|
||||||
await renameHandlerFunction(rename.fsPath, rename.from, rename.to);
|
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);
|
const contents = await readdir(directory);
|
||||||
// only delete an empty directory
|
// only delete an empty directory
|
||||||
// if it has contents, either something went wrong during cleanup or this
|
// if it has contents, either something went wrong during cleanup or this
|
||||||
@@ -595,18 +570,34 @@ async function cleanupFileSystem(
|
|||||||
await Promise.all(undoDirectoryPromises);
|
await Promise.all(undoDirectoryPromises);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function findGoModPath(workPath: string): Promise<string> {
|
/**
|
||||||
let checkPath = join(workPath, 'go.mod');
|
* Attempts to find a `go.mod` starting in the entrypoint directory and
|
||||||
if (await pathExists(checkPath)) {
|
* scanning up the directory tree.
|
||||||
return checkPath;
|
* @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');
|
return {
|
||||||
if (await pathExists(checkPath)) {
|
goModPath,
|
||||||
return checkPath;
|
isGoModInRootDir,
|
||||||
}
|
};
|
||||||
|
|
||||||
return '';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function isPortInfo(v: any): v is PortInfo {
|
function isPortInfo(v: any): v is PortInfo {
|
||||||
@@ -638,17 +629,159 @@ async function copyDevServer(
|
|||||||
await writeFile(join(dest, 'vercel-dev-server-main.go'), patched);
|
await writeFile(join(dest, 'vercel-dev-server-main.go'), patched);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function writeDefaultGoMod(
|
async function writeEntrypoint(
|
||||||
entrypointDirname: string,
|
dest: string,
|
||||||
packageName: 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'),
|
* Writes a `go.mod` file in the specified directory. If a `go.mod` file
|
||||||
defaultGoModContent,
|
* exists, then update the module name and any relative `replace` statements,
|
||||||
'utf-8'
|
* 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(
|
export async function startDevServer(
|
||||||
@@ -669,27 +802,26 @@ export async function startDevServer(
|
|||||||
const tmpPackage = join(tmp, entrypointDir);
|
const tmpPackage = join(tmp, entrypointDir);
|
||||||
await mkdirp(tmpPackage);
|
await mkdirp(tmpPackage);
|
||||||
|
|
||||||
let goModAbsPathDir = '';
|
const { goModPath } = await findGoModPath(
|
||||||
if (await pathExists(join(workPath, 'go.mod'))) {
|
join(workPath, entrypointDir),
|
||||||
goModAbsPathDir = workPath;
|
workPath
|
||||||
}
|
);
|
||||||
const analyzedRaw = await getAnalyzedEntrypoint(
|
const modulePath = goModPath ? dirname(goModPath) : undefined;
|
||||||
|
const analyzed = await getAnalyzedEntrypoint({
|
||||||
|
entrypoint: entrypointWithExt,
|
||||||
|
modulePath,
|
||||||
workPath,
|
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([
|
await Promise.all([
|
||||||
copyEntrypoint(entrypointWithExt, tmpPackage),
|
copyEntrypoint(entrypointWithExt, tmpPackage),
|
||||||
copyDevServer(analyzed.functionName, 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(
|
const portFile = join(
|
||||||
@@ -711,7 +843,7 @@ Learn more: https://vercel.com/docs/runtimes#official-runtimes/go`
|
|||||||
|
|
||||||
// build the dev server
|
// build the dev server
|
||||||
const go = await createGo({
|
const go = await createGo({
|
||||||
modulePath: goModAbsPathDir,
|
modulePath,
|
||||||
opts: {
|
opts: {
|
||||||
cwd: tmp,
|
cwd: tmp,
|
||||||
env,
|
env,
|
||||||
@@ -815,7 +947,7 @@ export async function prepareCache({
|
|||||||
//
|
//
|
||||||
// On the next build, the local cache will be restored and `createGo()` will
|
// 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`.
|
// 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);
|
const stat = await lstat(goCacheDir);
|
||||||
if (stat.isSymbolicLink()) {
|
if (stat.isSymbolicLink()) {
|
||||||
const goGlobalCacheDir = await readlink(goCacheDir);
|
const goGlobalCacheDir = await readlink(goCacheDir);
|
||||||
@@ -824,6 +956,6 @@ export async function prepareCache({
|
|||||||
await move(goGlobalCacheDir, goCacheDir);
|
await move(goGlobalCacheDir, goCacheDir);
|
||||||
}
|
}
|
||||||
|
|
||||||
const cache = await glob(`${cacheDir}/**`, workPath);
|
const cache = await glob(`${localCacheDir}/**`, workPath);
|
||||||
return cache;
|
return cache;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,10 +5,10 @@
|
|||||||
{ "src": "subdirectory/index.go", "use": "@vercel/go" }
|
{ "src": "subdirectory/index.go", "use": "@vercel/go" }
|
||||||
],
|
],
|
||||||
"probes": [
|
"probes": [
|
||||||
{ "path": "/", "mustContain": "cow:go1.20.1:RANDOMNESS_PLACEHOLDER" },
|
{ "path": "/", "mustContain": "cow:go1.20.2:RANDOMNESS_PLACEHOLDER" },
|
||||||
{
|
{
|
||||||
"path": "/subdirectory",
|
"path": "/subdirectory",
|
||||||
"mustContain": "subcow:go1.20.1:RANDOMNESS_PLACEHOLDER"
|
"mustContain": "subcow:go1.20.2:RANDOMNESS_PLACEHOLDER"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
module go-example
|
module go-example
|
||||||
|
|
||||||
go 1.12
|
go 1.20
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
module sub-1
|
module sub-1
|
||||||
|
|
||||||
go 1.12
|
go 1.20
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
module sub-2
|
module sub-2
|
||||||
|
|
||||||
go 1.12
|
go 1.20
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
module other-folder
|
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,
|
"version": 2,
|
||||||
"builds": [{ "src": "index.go", "use": "@vercel/go" }],
|
"builds": [{ "src": "index.go", "use": "@vercel/go" }],
|
||||||
"build": { "env": { "GO_BUILD_FLAGS": "-tags first -ldflags '-s -w'" } },
|
"build": { "env": { "GO_BUILD_FLAGS": "-tags first -ldflags '-s -w'" } }
|
||||||
"probes": [
|
|
||||||
{
|
|
||||||
"path": "/",
|
|
||||||
"mustContain": "version:go1.14.15:first:RANDOMNESS_PLACEHOLDER"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,4 +2,10 @@ module github.com/vercel/does-not-exist
|
|||||||
|
|
||||||
go 1.13
|
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": [
|
"probes": [
|
||||||
{
|
{
|
||||||
"path": "/api/v1/routes/someroute",
|
"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