@sparticuz/serverless-plugin-monorepo
Version:
OSS Serverless plugin to allow use inside a JS monorepo
128 lines (127 loc) • 5.45 kB
JavaScript
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, [])));
}
}