netlify-cli
Version:
Netlify command line tool
119 lines • 5.81 kB
JavaScript
import { createConnection } from 'net';
import { dirname } from 'path';
import { pathToFileURL } from 'url';
import { Worker } from 'worker_threads';
import lambdaLocal from 'lambda-local';
import { BLOBS_CONTEXT_VARIABLE } from '../../../blobs/blobs.js';
import detectNetlifyLambdaBuilder from './builders/netlify-lambda.js';
import detectZisiBuilder, { getFunctionMetadata } from './builders/zisi.js';
import { SECONDS_TO_MILLISECONDS } from './constants.js';
export const name = 'js';
let netlifyLambdaDetectorCache;
lambdaLocal.getLogger().level = 'alert';
// The netlify-lambda builder can't be enabled or disabled on a per-function
// basis and its detection mechanism is also quite expensive, so we detect
// it once and cache the result.
const detectNetlifyLambdaWithCache = async () => {
if (netlifyLambdaDetectorCache === undefined) {
netlifyLambdaDetectorCache = await detectNetlifyLambdaBuilder();
}
return netlifyLambdaDetectorCache;
};
export async function getBuildFunction({ config, directory, errorExit, func, projectRoot, }) {
const netlifyLambdaBuilder = await detectNetlifyLambdaWithCache();
if (netlifyLambdaBuilder) {
return netlifyLambdaBuilder.build;
}
const metadata = await getFunctionMetadata({ mainFile: func.mainFile, config, projectRoot });
const zisiBuilder = await detectZisiBuilder({ config, directory, errorExit, func, metadata, projectRoot });
if (zisiBuilder) {
return zisiBuilder.build;
}
// If there's no function builder, we create a simple one on-the-fly which
// returns as `srcFiles` the function directory, if there is one, or its
// main file otherwise.
const functionDirectory = dirname(func.mainFile);
const srcFiles = functionDirectory === directory ? [func.mainFile] : [functionDirectory];
const build = () => Promise.resolve({ schedule: metadata?.schedule, srcFiles });
return build;
}
const workerURL = new URL('worker.js', import.meta.url);
export const invokeFunction = async ({ context, environment, event, func, timeout, }) => {
const { buildData } = func;
// I have no idea why, but it appears that treating the case of a missing `buildData` or missing
// `buildData.runtimeAPIVersion` as V1 is important.
const runtimeAPIVersion = buildData != null && 'runtimeAPIVersion' in buildData && typeof buildData.runtimeAPIVersion === 'number'
? buildData.runtimeAPIVersion
: null;
if (runtimeAPIVersion == null || runtimeAPIVersion !== 2) {
return await invokeFunctionDirectly({ context, event, func, timeout });
}
const workerData = {
clientContext: JSON.stringify(context),
environment,
event,
// If a function builder has defined a `buildPath` property, we use it.
// Otherwise, we'll invoke the function's main file.
// Because we use import() we have to use file:// URLs for Windows.
entryFilePath: pathToFileURL(buildData != null && 'buildPath' in buildData && buildData.buildPath ? buildData.buildPath : func.mainFile).href,
timeoutMs: timeout * SECONDS_TO_MILLISECONDS,
};
const worker = new Worker(workerURL, { workerData });
return await new Promise((resolve, reject) => {
worker.on('message', (result) => {
// TODO(serhalp): Improve `WorkerMessage` type. It sure would be nice to keep it simple as it
// is now, but technically this is an arbitrary type from the user function return...
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (result?.streamPort != null) {
const client = createConnection({
port: result.streamPort,
host: 'localhost',
}, () => {
result.body = client;
resolve(result);
});
client.on('error', reject);
}
else {
resolve(result);
}
});
worker.on('error', reject);
});
};
export const invokeFunctionDirectly = async ({ context, event, func, timeout, }) => {
const buildData = await func.getBuildData();
if (buildData == null) {
throw new Error('Cannot invoke a function that has not been built');
}
// If a function builder has defined a `buildPath` property, we use it.
// Otherwise, we'll invoke the function's main file.
const lambdaPath = 'buildPath' in buildData && typeof buildData.buildPath === 'string' ? buildData.buildPath : func.mainFile;
const result = await lambdaLocal.execute({
clientContext: JSON.stringify(context),
environment: {
// We've set the Blobs context on the parent process, which means it will
// be available to the Lambda. This would be inconsistent with production
// where only V2 functions get the context injected. To fix it, unset the
// context variable before invoking the function.
// This has the side-effect of also removing the variable from `process.env`.
[BLOBS_CONTEXT_VARIABLE]: undefined,
},
event,
lambdaPath,
timeoutMs: timeout * SECONDS_TO_MILLISECONDS,
verboseLevel: 3,
esm: lambdaPath.endsWith('.mjs'),
});
return result;
};
export const onDirectoryScan = async () => {
const netlifyLambdaBuilder = await detectNetlifyLambdaWithCache();
// Before we start a directory scan, we check whether netlify-lambda is being
// used. If it is, we run it, so that the functions directory is populated
// with the compiled files before the scan begins.
if (netlifyLambdaBuilder) {
await netlifyLambdaBuilder.build();
}
};
//# sourceMappingURL=index.js.map