UNPKG

@kumologica/builder

Version:

Kumologica build and deploy module

450 lines (400 loc) 11.6 kB
const fs = require('fs-extra'); const path = require('path'); const { validateKumologicaFlow } = require('../code') function genLambdaJs(flowFileName) { return ` 'use strict'; const { LambdaFlowBuilder } = require('@kumologica/runtime'); const lambdaFlow = new LambdaFlowBuilder('${flowFileName}'); exports.handler = lambdaFlow.handler; `; } function genAzureFunctionJs(flowFileName) { return ` 'use strict'; const { AzureFuncFlowBuilder } = require('@kumologica/runtime'); const azureFuncFlow = new AzureFuncFlowBuilder('${flowFileName}'); exports.handler = azureFuncFlow.handler; `; } function getBuildDir(projectDir) { return path.join(projectDir, 'build'); } function genFlowJson() { return String.raw`[ { "id": "main.flow", "type": "tab", "label": "main", "disabled": false, "info": "" }, { "id": "test.flow", "type": "tab", "label": "test", "disabled": false, "info": "" }, { "id": "eacbb9ef.69d5c8", "type": "EventListener", "z": "main.flow", "name": "GET /hello", "provider": "aws", "eventSource": "api", "dynamodbOperation": "", "apiMethod": "get", "apiUrl": "/hello", "albMethod": "any", "albUrl": "", "bucketName": "", "event": "", "x": 120, "y": 160, "wires": [ [ "5eb3cd61.894f04" ] ], "caname": "event-handler", "category": "general" }, { "id": "5eb3cd61.894f04", "type": "Logger", "z": "main.flow", "name": "Log", "level": "INFO", "message": "Request received", "x": 310, "y": 160, "wires": [ [ "b5fc6ccd.17dd7" ] ], "caname": "logger", "category": "logging" }, { "id": "b5fc6ccd.17dd7", "type": "EventListener-End", "z": "main.flow", "name": "Success", "statusCode": "200", "headers": { "Content-Type": "application/json" }, "payload": "{\"hello\": \"world\"}", "x": 520, "y": 160, "wires": [], "caname": "eventlistenerend", "category": "general" }, { "id": "b970b7d2.5454c8", "type": "Assertion", "z": "test.flow", "name": "check response", "selector": "jsonBody", "property": "hello", "comparison": "equals", "value": "world", "x": 265, "y": 160, "wires": [ [ "e0721cc7.653eb" ] ], "caname": "test-assertion", "category": "testing" }, { "id": "e0721cc7.653eb", "type": "TestCaseEnd", "z": "test.flow", "name": "TestCaseEnd", "x": 425, "y": 160, "wires": [], "caname": "test-case-end", "category": "testing" }, { "id": "e8fb91dc.46c24", "type": "HTTPTestCase", "z": "test.flow", "name": "HTTPTestCase", "method": "GET", "path": "/hello", "headers": { "Accept": "application/json" }, "authtype": "none", "secUser": "", "secPassword": "", "secToken": "", "payload": "", "x": 122.5, "y": 160, "wires": [ [ "b970b7d2.5454c8" ] ], "caname": "http-test-case", "category": "testing" } ]`; } function sanitizeResourceName(name) { return name .trim() .replace(/^@/, '') .replace(/[^a-zA-Z0-9]/g, '') .substr(0, 140); } function sanitizeLambdaName(name) { return name .replace('.json', '') .trim() .replace(/^@/, '') .replace(/[^a-zA-Z0-9-]/g, '-') .substr(0, 140); } function sanitizeAzureFuntionName(name) { return name .replace('.json', '') .trim() .replace(/^@/, '') .replace(/[^a-zA-Z0-9]/g, '') .toLowerCase(); } function sanitizePackageJsonDescription(description) { return description.replace(/\n/g, '\\n').replace(/"/g, '\\"'); } function sanitizePackageJsonName(name) { return sanitizeLambdaName(name).toLowerCase(); } function genPackageJson(projectName, lambdaFileName, flowFileName, runtimeVersion, name, version, description, author, license) { return `{ "name": "${sanitizePackageJsonName(name || projectName)}", "version": "${version || "1.0.0"}", "description": "${sanitizePackageJsonDescription(description || "Kumologica flow")}", "main": "${lambdaFileName}", "files": [ "${lambdaFileName}", "${flowFileName}", "node_modules/**/*" ], "scripts": { }, "keywords": [], "author": "${author || ""}", "license": "${license || "ISC"}", "dependencies": { "@kumologica/runtime": "${runtimeVersion}" }, "devDependencies": { "@aws-sdk/client-cloudwatch-events": "^3.549.0", "@aws-sdk/client-dynamodb": "^3.540.0", "@aws-sdk/client-lambda": "^3.549.0", "@aws-sdk/client-rekognition": "^3.549.0", "@aws-sdk/client-s3": "^3.550.0", "@aws-sdk/client-ses": "^3.549.0", "@aws-sdk/client-sns": "^3.549.0", "@aws-sdk/client-sqs": "^3.549.0", "@aws-sdk/client-ssm": "^3.549.0", "@aws-sdk/client-sts": "^3.540.0", "@aws-sdk/lib-dynamodb": "^3.540.0" } } `; } function createFile(baseDir, fileName, content) { fs.outputFileSync(path.join(baseDir, fileName), content, 'utf-8'); } function createFileFullPath(pathFileName, content) { fs.outputFileSync(pathFileName, content, 'utf-8'); } function findFlowFile(projectDir) { return fs.readdirSync(projectDir).find(f => { return ( f.endsWith('.json') && f != 'template.json' && f !== 'package.json' && f !== 'package-lock.json' && f !== 'config.json' && loadAndValidateFlowFile(projectDir, f) ); }) } function loadAndValidateFlowFile(projectDir, file) { let f = fs.readFileSync(path.join(projectDir, file)); let content; try { content = JSON.parse(f); }catch(e){ content = '' } let response = validateKumologicaFlow(content); return response.valid; } function loadJsonFile(file) { return JSON.parse(fs.readFileSync(file)); } /** * * @param {*} projectName * @param {*} projectBasedir * @returns projectDir - absolute path of the path location for the new project */ function generateProjectCode(directory, flowFilename) { const lambdaFilename = 'lambda.js'; // create `lambda.js` file createFile(directory, lambdaFilename, genLambdaJs(flowFilename)); } function generateAzureProjectCode(directory, flowFilename) { const lambdaFilename = 'index.js'; // create `lambda.js` file createFile(directory, lambdaFilename, genAzureFunctionJs(flowFilename)); } /** * * @param {*} options { basePath, projectName, runtimeVersion} */ function generateBoilerplateProject(options) { const { basePath, projectName, runtimeVersion } = options; generateInitialCode(basePath, projectName, undefined, runtimeVersion); } // function creates initial flow and package.json function generateInitialCode(projectBasedir, projectName, flow, runtimeVersion) { const defaultFlowFilename = `${projectName}-flow.json`; const projectDir = path.join(projectBasedir, projectName); // create the project directory if (fs.existsSync(projectDir) && projectName !== '__untitled__') { throw new Error(`"${projectDir}" directory already exists`) } else { fs.mkdirpSync(projectDir); // create `{project}-flow.json` file createFile(projectDir, defaultFlowFilename, flow ? flow : genFlowJson()); // create `package.json` file createFile( projectDir, 'package.json', genPackageJson(projectName, 'lambda.js', defaultFlowFilename, runtimeVersion) ); return projectDir; } } function findTestCasesFromFlow(flowFilePath) { const flowFileData = fs.readFileSync(flowFilePath, 'utf-8'); const flowFileJson = JSON.parse(flowFileData); return flowFileJson.filter((node) => node.type === 'TestCase' || node.type === 'HTTPTestCase'); } async function removeSymlinks(directory, recursively = false) { const fsp = require("fs").promises; console.log(`Checking for symlinks in: ${directory}`); try { const files = await fsp.readdir(directory); for (const file of files) { const filePath = path.join(directory, file); const stats = await fsp.lstat(filePath); if (stats.isSymbolicLink()) { console.warn(`Removing symlink: ${filePath}`); await fsp.unlink(filePath); } else if (stats.isDirectory() && recursively) { await removeSymlinks(filePath); // Recursively check subdirectories } } } catch (err) { console.error(`Error scanning directory: ${err.message}`); } } async function zipDirectory(buildDir, zipFilePath) { const yazl = require('yazl'); console.log('Zipping ...'); // Ensure output directory exists await fs.ensureDir(path.dirname(zipFilePath)); return new Promise((resolve, reject) => { const zipfile = new yazl.ZipFile(); const output = fs.createWriteStream(zipFilePath); output.on('close', () => { console.log(`Archive created: ${zipFilePath}`); resolve(); }); output.on('error', reject); zipfile.outputStream.pipe(output); // Recursively walk the directory (function addDirContents(dir, relPath = '') { const items = fs.readdirSync(dir); for (const item of items) { const fullPath = path.join(dir, item); const relItemPath = path.join(relPath, item); const stats = fs.statSync(fullPath); if (stats.isDirectory()) { addDirContents(fullPath, relItemPath); } else { zipfile.addFile(fullPath, relItemPath); } } })(buildDir); // End the zip file (Zip64 enabled by default in yazl) zipfile.end(); }); } async function extractEntryFromZip(zipFilePath, entryName, destDir) { const yauzl = require('yauzl'); return new Promise((resolve, reject) => { yauzl.open(zipFilePath, { lazyEntries: true }, (err, zipfile) => { if (err) return reject(err); zipfile.readEntry(); zipfile.on('entry', (entry) => { if (entry.fileName === entryName) { zipfile.openReadStream(entry, (err, readStream) => { if (err) return reject(err); const destPath = path.join(destDir, entryName); readStream.pipe(fs.createWriteStream(destPath)); readStream.on('end', () => { zipfile.close(); resolve(); }); }); } else { zipfile.readEntry(); } }); zipfile.on('end', () => { reject(new Error(`Entry ${entryName} not found in zip file.`)); }); }); }); } // Usage: // // Run cleanup //removeSymlinks("./node_modules") // .then(() => console.log(" Symlink cleanup complete.")) // .catch(console.error); module.exports = { generateProjectCode, generateAzureProjectCode, generateBoilerplateProject, generateInitialCode, createFile, createFileFullPath, findFlowFile, loadJsonFile, sanitizeLambdaName, sanitizeAzureFuntionName, sanitizeResourceName, getBuildDir, findTestCasesFromFlow, removeSymlinks, genPackageJson, zipDirectory, extractEntryFromZip };