UNPKG

@sparticuz/serverless-plugin-monorepo

Version:
128 lines (127 loc) 5.45 kB
import fs from "fs-extra"; import * as path from "node:path"; /** Takes a path and returns all node_modules resolution paths (but not global include paths). */ function getNodeModulePaths(p) { const result = []; const paths = p.split(path.sep); while (paths.length) { result.push(path.join(paths.join(path.sep) || path.sep, "node_modules")); paths.pop(); } return result; } /** Creates a symlink. Ignore errors if symlink exists or package exists. */ async function link(target, f, type) { await fs.ensureDir(path.dirname(f)); // eslint-disable-next-line @typescript-eslint/use-unknown-in-catch-callback-variable await fs.symlink(target, f, type).catch((e) => { if (e.code) { if (e.code === "EEXIST" || e.code === "EISDIR") { return; } } throw e; }); } /** Plugin implementation */ export default class ServerlessMonoRepo { serverless; hooks; constructor(serverless) { this.serverless = serverless; this.hooks = { "package:cleanup": () => void this.clean(), "package:initialize": () => void this.initialise(), "before:offline:start:init": () => void this.initialise(), "offline:start": () => void this.initialise(), "deploy:function:initialize": () => void (async () => { await this.clean(); await this.initialise(); })(), }; // Settings serverless.configSchemaHandler.defineCustomProperties({ type: "object", properties: { monorepo: { type: "object", properties: { path: { type: "string", default: this.serverless.config.servicePath, }, linkType: { type: "string", enum: ["junction", "dir", "file"], default: "junction", }, }, }, }, }); } async linkPackage(name, fromPath, toPath, created, resolved) { // Ignore circular dependencies if (resolved.includes(name)) { return; } // Obtain list of module resolution paths to use for resolving modules const paths = getNodeModulePaths(fromPath); // Get package file path const pkg = require.resolve("./" + path.join(name, "package.json"), { paths, }); // Get relative path to package & create link if not an embedded node_modules const target = path.relative(path.join(toPath, path.dirname(name)), path.dirname(pkg)); if ((pkg.match(/node_modules/g) ?? []).length <= 1 && !created.has(name)) { created.add(name); await link(target, path.join(toPath, name), this.serverless.service.custom.linkType); } // Get dependencies const packageData = (await fs.readJson(pkg)); const { dependencies = {} } = packageData; // Link all dependencies await Promise.all(Object.keys(dependencies).map((dep) => this.linkPackage(dep, path.dirname(pkg), toPath, created, resolved.concat([name])))); } async clean() { // Remove all symlinks that are of form [...]/node_modules/link console.log("Cleaning dependency symlinks"); // Checks if a given stat result indicates a scoped package directory const isScopedPkgDir = (c) => c.s.isDirectory() && c.f.startsWith("@"); // Cleans all links in a specific path async function cleanAllLinks(p) { if (!(await fs.pathExists(p))) { return; } const files = await fs.readdir(p); let contents = await Promise.all(files.map((f) => fs.lstat(path.join(p, f)).then((s) => ({ f, s })))); // Remove all links await Promise.all(contents .filter((c) => c.s.isSymbolicLink()) .map((c) => fs.unlink(path.join(p, c.f)))); contents = contents.filter((c) => !c.s.isSymbolicLink()); // Remove all links in scoped packages await Promise.all(contents .filter(isScopedPkgDir) .map((c) => cleanAllLinks(path.join(p, c.f)))); contents = contents.filter((c) => !isScopedPkgDir(c)); // Remove directory if empty const filesInDir = await fs.readdir(p); if (!filesInDir.length) { await fs.rmdir(p); } } // Clean node_modules await cleanAllLinks(path.join(this.serverless.service.custom.path, "node_modules")); } async initialise() { // Read package JSON const packageJsonPath = path.join(this.serverless.service.custom.path, "package.json"); const packageJson = (await fs.readJson(packageJsonPath)); const { dependencies = {} } = packageJson; // Link all dependent packages console.log("Creating dependency symlinks"); const contents = new Set(); await Promise.all(Object.keys(dependencies).map((name) => this.linkPackage(name, this.serverless.service.custom.path, path.join(this.serverless.service.custom.path, "node_modules"), contents, []))); } }