@seasketch/geoprocessing
Version:
Geoprocessing and reporting framework for SeaSketch 2.0
149 lines • 7.73 kB
JavaScript
import fs from "fs-extra";
import path from "node:path";
import * as esbuild from "esbuild";
import { generateManifest } from "./generateManifest.js";
import { getHandlerFilenameFromSrcPath } from "../util/handler.js";
import { generateHandler } from "./generateHandler.js";
/**
* Builds project geoprocessing and preprocessing functions
* @param projectPath - project path containing package.json, geoprocessing.json, and geoprocessing or preprocessing functions
* @param destBuildPath - path to write bundled functions to
*/
export async function buildProjectFunctions(projectPath, destBuildPath) {
if (!fs.existsSync(destBuildPath)) {
fs.mkdirSync(destBuildPath);
}
const geoprocessing = JSON.parse(fs
.readFileSync(path.join(projectPath, "project", "geoprocessing.json"))
.toString());
const packageGp = JSON.parse(fs.readFileSync("./package.json").toString());
const packageProject = JSON.parse(fs.readFileSync(path.join(projectPath, "package.json")).toString());
if (!geoprocessing.preprocessingFunctions &&
!geoprocessing.geoprocessingFunctions) {
throw new Error("No functions found in geoprocessing.json");
}
// For each project function generate root lambda function and bundle into single JS file with all dependencies
const functionPaths = [
...geoprocessing.geoprocessingFunctions,
...geoprocessing.preprocessingFunctions,
];
if (process.env.NODE_ENV !== "test")
console.log("Bundling functions found in geoprocessing.json...\n");
await Promise.all(functionPaths.map(async (functionPath) => {
// generate function (lambda handler) that wraps the preprocessing or geoprocessing function
// the handler is the entry point for the lmabda and receives the event (payload) from the API gateway
// and passes it on
const handlerPath = generateHandler(path.join(projectPath, functionPath), destBuildPath);
const handlerDestPath = `${path.basename(functionPath)}`.replace(".ts", "Handler.mjs");
// Build a local npm package for the function
const pkgName = handlerDestPath.replace(".mjs", "");
const pkgPath = path.join(destBuildPath, pkgName).replace("Handler", "");
const bundledPath = path.join(pkgPath, handlerDestPath);
fs.ensureDirSync(pkgPath);
// Build esm bundle with all dependencies
const buildResult = await esbuild.build({
entryPoints: [handlerPath],
bundle: true,
outfile: bundledPath,
platform: "node",
format: "esm",
logLevel: process.env.NODE_ENV === "test" ? "error" : "info",
sourcemap: false,
external: ["aws-cdk-lib", "aws-sdk"],
banner: {
// workaround require bug https://github.com/evanw/esbuild/pull/2067#issuecomment-1324171716
js: "import { createRequire } from 'module'; const require = createRequire(import.meta.url);",
},
});
if (buildResult.errors.length > 0 || buildResult.warnings.length > 0) {
console.log(JSON.stringify(buildResult, null, 2));
}
// package.json with type: module, needed in the build directory containing handlers in order to enable ESM runtime in lambda
// WRITE A FUNCTION THAT GENERATES PACKAGE.JSON that points to handler
fs.writeJSONSync(path.join(pkgPath, "package.json"), {
name: handlerDestPath,
type: "module",
description: "This package will be treated as an ES module.",
version: "1.0",
main: handlerDestPath,
}, { spaces: 2 });
}));
// package.json with type: module, needed in the build directory containing handlers in order to enable ESM runtime in lambda
fs.writeJSONSync(path.join(destBuildPath, "package.json"), {
name: "gp-project-build",
type: "module",
description: "This package will be treated as an ES module.",
version: "1.0",
main: "index.js",
}, { spaces: 2 });
// OTHER_FUNCTIONS
if (process.env.NODE_ENV !== "test")
console.log("\nBundling support functions...\n");
const otherFunctions = [
"src/aws/serviceHandlers.ts",
"src/sockets/sendmessage.ts",
"src/sockets/connect.ts",
"src/sockets/disconnect.ts",
];
await Promise.all(otherFunctions.map(async (functionPath) => {
const bundledName = path.basename(functionPath).replace(".ts", ".mjs");
const functionName = bundledName.replace(".mjs", "");
const bundledPath = path.join(destBuildPath, functionName, bundledName);
const pkgPath = path.join(destBuildPath, functionName);
const minify = process.env.NOMINIFY ? false : true;
const buildResult = await esbuild.build({
entryPoints: [functionPath],
bundle: true,
outfile: bundledPath,
platform: "node",
format: "esm",
logLevel: process.env.NODE_ENV === "test" ? "error" : "info",
minify: minify,
treeShaking: true,
metafile: true,
sourcemap: false,
external: ["aws-cdk-lib"], // keep aws-sdk until migrate to v3 built into lambda
banner: {
// workaround require bug https://github.com/evanw/esbuild/pull/2067#issuecomment-1324171716
js: "import { createRequire } from 'module'; const require = createRequire(import.meta.url);",
},
});
if (process.env.NODE_ENV !== "test" &&
(buildResult.errors.length > 0 || buildResult.warnings.length > 0)) {
console.log(JSON.stringify(buildResult, null, 2));
}
if (buildResult.metafile && process.env.ANALYZE) {
if (process.env.NODE_ENV !== "test")
console.log("Generating metafile esbuild-metafile-lambda.json"); // use https://bundle-buddy.com/esbuild to analyze
await fs.writeFile(`${projectPath}/esbuild-metafile-lambda.json`, JSON.stringify(buildResult.metafile));
}
// package.json with type: module (to enable ESM) and entry point to lambda hander
fs.writeJSONSync(path.join(pkgPath, "package.json"), {
name: bundledName,
type: "module",
description: "This package will be treated as an ES module.",
version: "1.0",
main: bundledName,
}, { spaces: 2 });
}));
// MANIFEST
/**
* Given full path to source geoprocessing function, requires and returns its pre-generated handler module
*/
async function getHandlerModule(srcFuncPath) {
const name = getHandlerFilenameFromSrcPath(srcFuncPath);
const p = path.join(destBuildPath, name);
return await import(p);
}
const preprocessingBundles = geoprocessing.preprocessingFunctions &&
(await Promise.all(geoprocessing.preprocessingFunctions.map(getHandlerModule)));
const geoprocessingBundles = geoprocessing.geoprocessingFunctions &&
(await Promise.all(geoprocessing.geoprocessingFunctions.map(getHandlerModule)));
const manifest = generateManifest(geoprocessing, packageProject, preprocessingBundles, geoprocessingBundles, packageGp.version);
const manifestPath = path.join(destBuildPath, "manifest.json");
if (process.env.NODE_ENV !== "test")
console.log(`\nCreating service manifest ${manifestPath}\n`);
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, " "));
fs.copyFileSync(manifestPath, path.join(destBuildPath, "serviceHandlers", "manifest.json"));
}
//# sourceMappingURL=buildProjectFunctions.js.map