UNPKG

@sparticuz/serverless-plugin-monorepo

Version:
212 lines (187 loc) 5.85 kB
import fs, { type SymlinkType } from "fs-extra"; import * as path from "node:path"; import type Serverless from "serverless"; /** Takes a path and returns all node_modules resolution paths (but not global include paths). */ function getNodeModulePaths(p: string): string[] { const result: string[] = []; 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: string, f: string, type: fs.SymlinkType) { 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: { code: string }) => { if (e.code) { if (e.code === "EEXIST" || e.code === "EISDIR") { return; } } throw e; }); } interface PackageJson { name: string; version: string; dependencies?: Record<string, string>; devDependencies?: Record<string, string>; peerDependencies?: Record<string, string>; optionalDependencies?: Record<string, string>; } /** Plugin implementation */ export default class ServerlessMonoRepo { hooks: Record<string, () => void>; constructor(private 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: string, fromPath: string, toPath: string, created: Set<string>, resolved: string[], ) { // 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 as SymlinkType, ); } // Get dependencies const packageData = (await fs.readJson(pkg)) as PackageJson; 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"); interface File { f: string; s: fs.Stats; } // Checks if a given stat result indicates a scoped package directory const isScopedPkgDir = (c: File) => c.s.isDirectory() && c.f.startsWith("@"); // Cleans all links in a specific path async function cleanAllLinks(p: string) { if (!(await fs.pathExists(p))) { return; } const files = await fs.readdir(p); let contents: File[] = 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 as string, "node_modules"), ); } async initialise() { // Read package JSON const packageJsonPath = path.join( this.serverless.service.custom.path as string, "package.json", ); const packageJson = (await fs.readJson(packageJsonPath)) as PackageJson; const { dependencies = {} } = packageJson; // Link all dependent packages console.log("Creating dependency symlinks"); const contents = new Set<string>(); await Promise.all( Object.keys(dependencies).map((name) => this.linkPackage( name, this.serverless.service.custom.path as string, path.join( this.serverless.service.custom.path as string, "node_modules", ), contents, [], ), ), ); } }