UNPKG

@webda/shell

Version:

Deploy a Webda app or configure it

342 lines 12.9 kB
import { JSONUtils } from "@webda/core"; import * as crypto from "crypto"; import * as fs from "fs"; import { globSync } from "glob"; import * as path from "path"; import * as semver from "semver"; import { intersect } from "semver-intersect"; import { Deployer } from "./deployer.js"; /** * Generate a ZIP Package of the application * * It can add a file as entrypoint * * @param zipPath path to store the package * @param entrypoint file to integrate as entrypoint.js * @WebdaDeployer WebdaDeployer/Packager */ export default class Packager extends Deployer { constructor() { super(...arguments); this.packagesGenerated = {}; } static loadPackageInfo(dir) { return JSONUtils.loadFile(path.join(dir, "package.json")); } static getWorkspacesPackages(dir = "") { if (dir === "") { dir = process.cwd(); } let result = this.loadPackageInfo(dir).workspaces || ["packages/*"]; return result .map(r => globSync(path.join(dir, r))) .flat() .map(r => path.relative(dir, r)); } /** * Retrieve Yarn workspaces root */ static getWorkspacesRoot(dir = "") { if (dir === "") { dir = process.cwd(); } do { if (fs.existsSync(path.join(dir, "lerna.json"))) { return dir; } if (fs.existsSync(path.join(dir, "package.json"))) { try { let pkg = JSONUtils.loadFile(path.join(dir, "package.json")); if (pkg.workspaces) { return dir; } } catch (err) { } } // Does not make sense to go upper than the .git repo if (fs.existsSync(path.join(dir, ".git")) || path.resolve(dir) === "/") { return undefined; } dir = path.join(dir, ".."); } while (fs.existsSync(dir)); } static getPackageLastChanges(pkg, includeWorkspace = false) { let hash = crypto.createHash("md5"); if (includeWorkspace) { let root = this.getWorkspacesRoot(pkg); if (root) { this.getWorkspacesPackages(root).forEach(p => { if (fs.existsSync(path.join(root, p, "package.json"))) { hash.update(this.getPackageLastChanges(path.join(root, p), false)); } }); return hash.digest("hex"); } } let main = Packager.loadPackageInfo(pkg); main.files ?? (main.files = []); main.files.forEach(p => { let includeDir = path.join(pkg, p); if (fs.existsSync(includeDir)) { globSync(includeDir).forEach(src => { let stat = fs.lstatSync(src); if (stat.isDirectory()) { return globSync(src + "/**").forEach(f => hash.update(fs.lstatSync(f).mtime + f)); } else { hash.update(stat.mtime + src); } }); } }); return hash.digest("hex"); } static getDependencies(pkg) { let deps = {}; let wrk = Packager.getWorkspacesRoot(); let main = Packager.loadPackageInfo(pkg); main.resolutions = main.resolutions || {}; const browsed = []; let browse = (p, depth) => { if (browsed.includes(p)) return; browsed.push(p); let info = Packager.loadPackageInfo(p); info.dependencies = info.dependencies || {}; Object.keys(info.dependencies).forEach(name => { let version = info.dependencies[name]; if (main.resolutions[name]) { version = main.resolutions[name]; } deps[name] = deps[name] || []; deps[name].push({ name: p, version }); // Is there any specific version for this package if (fs.existsSync(`${p}/node_modules/${name}`)) { browse(`${p}/node_modules/${name}`, depth + 1); // Is it in the direct deps } else if (fs.existsSync(`node_modules/${name}`)) { browse(`node_modules/${name}`, depth + 1); // Is there a workspace dep existing } else if (wrk && fs.existsSync(`${wrk}/node_modules/${name}`)) { browse(`${wrk}/node_modules/${name}`, depth + 1); } }); }; browse(pkg, 0); return deps; } static getResolvedDependencies(pkg) { const deps = Packager.getDependencies(pkg); let resolutions = {}; for (let i in deps) { if (deps[i].length > 1) { try { resolutions[i] = intersect(...deps[i].filter(v => semver.validRange(v.version) && v.version !== "*").map(v => v.version)); } catch (err) { console.log("Cannot simplify", i, deps[i]); } } else { resolutions[i] = deps[i][0].version; } } return resolutions; } async loadDefaults() { var _a; await super.loadDefaults(); this.resources.package = this.resources.package || {}; this.resources.package.ignores = this.resources.package.ignores || []; this.resources.package.excludePatterns = this.resources.package.excludePatterns || ["\\.d\\.ts$"]; this.resources.package.modules = this.resources.package.modules || {}; this.resources.package.modules.excludes = this.resources.package.modules.excludes || []; this.resources.package.modules.includes = this.resources.package.modules.includes || []; (_a = this.resources).zipPath ?? (_a.zipPath = this.app.getAppPath("/dist/package.zip")); // Append .zip if not there if (!this.resources.zipPath.endsWith(".zip")) { this.resources.zipPath += ".zip"; } } /** * Import the archiver * @returns */ async getArchiver() { const { default: archiver } = await import("archiver"); return archiver("zip"); } /** * Generate a full code package including dependencies */ async deploy() { let { zipPath, entrypoint } = this.resources; if (this.packagesGenerated[zipPath + entrypoint || ""]) { return; } await this.app.load(); this.app.compile(); this.app.generateModule(); let targetDir = path.dirname(zipPath); if (!fs.existsSync(targetDir)) { fs.mkdirSync(targetDir); } if (fs.existsSync(zipPath)) { fs.unlinkSync(zipPath); } let ignores = [ "dist", "bin", "test", "Dockerfile", "README.md", "package.json", "deployments", "app", "webda.config.json", ...this.resources.package.ignores ]; // Should load the ignore from a file let toPacks = []; let files; let appPath = this.app.getAppPath(); let packageFile = this.app.getAppPath("package.json"); if (fs.existsSync(packageFile)) { files = JSONUtils.loadFile(packageFile).files; } files ?? (files = fs.readdirSync(appPath)); for (let i in files) { let name = files[i]; if (name.startsWith(".")) continue; if (ignores.indexOf(name) >= 0) continue; toPacks.push(`${appPath}/${name}`); } if (toPacks.indexOf(`${appPath}/node_modules`) >= 0) { toPacks = toPacks.filter(p => p !== `${appPath}/node_modules`); } // Ensure dependencies // Get deps info // Include specified modules let deps = [...this.resources.package.modules.includes]; deps.push(...Object.keys(Packager.getDependencies(appPath))); // Remove any excludes modules this.resources.package.modules.excludes.forEach(i => { let id = deps.indexOf(i); if (id >= 0) { deps.splice(id, 1); } }); // Include workspace deps let workspace = Packager.getWorkspacesRoot(this.app.getAppPath()); deps.forEach(dep => { // Include package dep if (fs.existsSync(`${appPath}/node_modules/${dep}`)) { toPacks.push(`${appPath}/node_modules/${dep}`); } else if (workspace && fs.existsSync(`${workspace}/node_modules/${dep}`)) { toPacks.push(`${workspace}/node_modules/${dep}`); } else { this.logger.log("WARN", "Cannot find package", dep); } }); let output = fs.createWriteStream(zipPath); const archive = await this.getArchiver(); return new Promise((resolve, reject) => { output.on("close", () => { this.packagesGenerated[zipPath + entrypoint || ""] = true; resolve(); }); archive.on("error", function (err) { console.log(err); reject(err); }); // Patch the archiver to allow filtering const originalFile = archive._append; archive._append = (from, to) => { let name = from; let exclude = false; this.resources.package.excludePatterns.forEach(r => { if (exclude || new RegExp(r).exec(from)) { exclude = true; } }); if (exclude) { this.logger.log("INFO", "Skipping ", name); return to.callback(); } return originalFile.call(archive, from, to); }; archive.pipe(output); for (let i in toPacks) { let stat = fs.lstatSync(toPacks[i]); let dstPath = path.relative(appPath, toPacks[i]).replace(/\.\.\//g, ""); if (stat.isSymbolicLink() && this.resources.includeLinkModules) { this.addLinkPackage(archive, fs.realpathSync(toPacks[i]), dstPath); } else if (stat.isDirectory()) { // Add custom recursive function archive.directory(toPacks[i], dstPath); } else if (stat.isFile()) { archive.file(toPacks[i], { name: path.relative(appPath, dstPath) }); } } if (entrypoint) { if (fs.existsSync(entrypoint)) { archive.file(entrypoint, { name: "entrypoint.js" }); } else { throw Error("Cannot find the entrypoint for Packager: " + entrypoint); } } archive.append(JSON.stringify(this.getPackagedConfiguration(), undefined, 2), { name: "webda.config.json" }); archive.finalize(); }); } getPackagedConfiguration() { let config = this.app.getCurrentConfiguration(); config = this.replaceVariables(config); config.cachedModules = this.app.getModules(); return config; } /** * Add a symbolic linked package from dependency * * @param archive to add to * @param fromPath absolutePath of package * @param toPath relative path within archive */ addLinkPackage(archive, fromPath, toPath) { let packageFile = fromPath + "/package.json"; let files; if (fs.existsSync(packageFile)) { archive.file(`${packageFile}`, { name: `${toPath}/package.json` }); files = JSONUtils.loadFile(packageFile).files; } files ?? (files = fs.readdirSync(fromPath)); files.forEach(file => { if (file.startsWith(".") || file === "package.json") return; let stat = fs.lstatSync(`${fromPath}/${file}`); if (stat.isDirectory()) { archive.directory(`${fromPath}/${file}`, `${toPath}/${file}`); } else if (stat.isFile()) { archive.file(`${fromPath}/${file}`, { name: `${toPath}/${file}` }); } }); } } export { Packager }; //# sourceMappingURL=packager.js.map