UNPKG

fixturify-project

Version:

[![npm version](https://badge.fury.io/js/fixturify-project.svg)](https://badge.fury.io/js/fixturify-project) [![CI](https://github.com/stefanpenner/node-fixturify-project/workflows/CI/badge.svg)](https://github.com/stefanpenner/node-fixturify-project/acti

605 lines 19.2 kB
// src/index.ts import fixturify from "fixturify"; import tmp from "tmp"; import fs from "fs-extra"; import path from "path"; import resolvePackagePath from "resolve-package-path"; import CacheGroup from "resolve-package-path/lib/cache-group.js"; import binLinks from "bin-links"; import walkSync from "walk-sync"; import deepmerge from "deepmerge"; import { findWorkspaceDir } from "@pnpm/find-workspace-dir"; import { findWorkspacePackages } from "@pnpm/workspace.find-packages"; import { packlist } from "@pnpm/fs.packlist"; import { createRequire } from "module"; var require2 = createRequire(import.meta.url); var { PackageCache } = require2("@embroider/shared-internals"); tmp.setGracefulCleanup(); var defaultFiles = { "index.js": ` 'use strict'; module.exports = {};` }; var Project = class { constructor(first, second, third, fourth) { this.isDependency = true; this._dependencies = {}; this._devDependencies = {}; this.dependencyLinks = /* @__PURE__ */ new Map(); this.linkIsDevDependency = /* @__PURE__ */ new Set(); this.usingHardLinks = true; this.resolutionCache = new CacheGroup(); this.transitivePeersCache = /* @__PURE__ */ new Map(); this.knownWorkspaces = /* @__PURE__ */ new Map(); let name; let version; let files; let requestedRange; if (first == null) { } else if (typeof first === "string") { name = first; if (typeof second === "string") { version = second; if (third) { if (!isProjectCallback(third)) { ({ files, requestedRange } = third); } } } else { if (second) { if (!isProjectCallback(second)) { ({ version, files, requestedRange } = second); } } } } else { ({ name, version, files, requestedRange } = first); } let pkg = {}; if (files && typeof (files == null ? void 0 : files["package.json"]) === "string") { pkg = JSON.parse(files["package.json"]); files = deepmerge({}, files); delete files["package.json"]; } this.pkg = Object.assign({}, pkg, { name: name || pkg.name || "a-fixturified-project", version: version || pkg.version || "0.0.0", keywords: pkg.keywords || [] }); if (files) { this.files = deepmerge({}, { ...defaultFiles, ...files }); } else { this.files = deepmerge({}, defaultFiles); } this.requestedRange = requestedRange || this.pkg.version; const arity = arguments.length; if (arity > 1) { fourth; const projectCallback = arguments[arity - 1]; if (isProjectCallback(projectCallback)) { projectCallback(this); } } } set baseDir(dir) { if (this._baseDir) { throw new Error(`this Project already has a baseDir`); } this._baseDir = dir; } get baseDir() { if (!this._baseDir) { this._tmp = tmp.dirSync({ unsafeCleanup: true }); this._baseDir = fs.realpathSync(this._tmp.name); } return this._baseDir; } get name() { return getPackageName(this.pkg); } set name(value) { this.pkg.name = value; } get version() { return getPackageVersion(this.pkg); } set version(value) { this.pkg.version = value; } static fromDir(baseDir, opts) { let project = new Project(); project.readSync(baseDir, opts); return project; } mergeFiles(dirJSON) { this.files = deepmerge(this.files, dirJSON); } async write(dirJSON) { if (dirJSON) { this.mergeFiles(dirJSON); } await this.writeProject(); await this.binLinks(); } addDependency(first, second, third, fourth) { let projectCallback; const arity = arguments.length; if (arity > 1) { fourth; const maybeProjectCallback = arguments[arity - 1]; if (isProjectCallback(maybeProjectCallback)) { projectCallback = maybeProjectCallback; } } if (isProjectCallback(second)) { second = void 0; } if (isProjectCallback(third)) { third = void 0; } return this.addDep(first, second, third, "_dependencies", projectCallback); } addDevDependency(first, second, third, fourth) { let projectCallback; const arity = arguments.length; if (arity > 1) { fourth; const maybeProjectCallback = arguments[arity - 1]; if (isProjectCallback(maybeProjectCallback)) { projectCallback = maybeProjectCallback; } } if (isProjectCallback(second)) { second = void 0; } if (isProjectCallback(third)) { third = void 0; } return this.addDep(first, second, third, "_devDependencies", projectCallback); } removeDependency(name) { delete this._dependencies[name]; this.dependencyLinks.delete(name); this.linkIsDevDependency.delete(name); } removeDevDependency(name) { delete this._devDependencies[name]; this.dependencyLinks.delete(name); this.linkIsDevDependency.delete(name); } linkDependency(name, opts) { this.removeDependency(name); this.removeDevDependency(name); this.dependencyLinks.set(name, opts); } linkDevDependency(name, opts) { this.linkDependency(name, opts); this.linkIsDevDependency.add(name); } dependencyProjects() { return Object.keys(this._dependencies).map((dependency) => this._dependencies[dependency]); } devDependencyProjects() { return Object.keys(this._devDependencies).map((dependency) => this._devDependencies[dependency]); } clone() { let cloned = new this.constructor(); cloned.pkg = JSON.parse(JSON.stringify(this.pkg)); cloned.files = JSON.parse(JSON.stringify(this.files)); for (let [name, depProject] of Object.entries(this._dependencies)) { cloned._dependencies[name] = depProject.clone(); } for (let [name, depProject] of Object.entries(this._devDependencies)) { cloned._devDependencies[name] = depProject.clone(); } cloned.dependencyLinks = new Map(this.dependencyLinks); cloned.linkIsDevDependency = new Set(this.linkIsDevDependency); cloned.requestedRange = this.requestedRange; return cloned; } dispose() { if (this._tmp) { this._tmp.removeCallback(); } } async writeProject() { this.assignBaseDirs(); let resolvedLinksMap = /* @__PURE__ */ new Map(); await this.discoverWorkspaces(); this.writeFiles(resolvedLinksMap); await this.finalizeWrite(resolvedLinksMap); } assignBaseDirs() { this.baseDir; for (let depList of [this.dependencyProjects(), this.devDependencyProjects()]) { for (let dep of depList) { dep.baseDir = path.join(this.baseDir, "node_modules", dep.name); dep.assignBaseDirs(); } } } writeFiles(resolvedLinksMap) { fixturify.writeSync(this.baseDir, this.files); for (let depList of [this.dependencyProjects(), this.devDependencyProjects()]) { for (let dep of depList) { dep.writeFiles(resolvedLinksMap); } } let resolvedLinks = this.resolveLinks(); fs.outputJSONSync(path.join(this.baseDir, "package.json"), this.pkgJSONWithDeps(resolvedLinks), { spaces: 2 }); resolvedLinksMap.set(this, resolvedLinks); } async finalizeWrite(resolvedLinksMap) { for (let [name, { dir: target }] of resolvedLinksMap.get(this)) { await this.writeLinkedPackage(name, target, path.join(this.baseDir, "node_modules", name)); } for (let depList of [this.dependencyProjects(), this.devDependencyProjects()]) { for (let dep of depList) { await dep.finalizeWrite(resolvedLinksMap); } } } resolveLinks() { return new Map( [...this.dependencyLinks.entries()].map(([name, opts]) => { let dir; if ("baseDir" in opts) { let pkgJSONPath = resolvePackagePath(opts.resolveName || name, opts.baseDir, this.resolutionCache); if (!pkgJSONPath) { throw new Error(`failed to locate ${opts.resolveName || name} in ${opts.baseDir}`); } dir = path.dirname(pkgJSONPath); } else if ("target" in opts) { dir = opts.target; } else { dir = opts.project.baseDir; } let requestedRange; if (opts.requestedRange) { requestedRange = opts.requestedRange; } else if ("target" in opts || "baseDir" in opts) { requestedRange = fs.readJsonSync(path.join(dir, "package.json")).version; } else { requestedRange = opts.project.version; } return [name, { requestedRange, dir }]; }) ); } async binLinks() { let nodeModules = path.join(this.baseDir, "node_modules"); for (const { pkg, path: path2 } of readPackages(nodeModules)) { await binLinks({ pkg, path: path2, top: false, global: false, force: true }); } } transitivePeers(dir) { let peers = this.transitivePeersCache.get(dir); if (peers) { return peers; } peers = /* @__PURE__ */ new Set(); this.transitivePeersCache.set(dir, peers); let pkg = PackageCache.shared("fixturify-project", this.baseDir).get(dir); let deps = pkg.dependencies; for (let dep of deps) { if (pkg.categorizeDependency(dep.name) === "dependencies") { for (let peer of this.transitivePeers(dep.root)) { if (pkg.hasDependency(peer)) { } else { peers.add(peer); } } } } for (let dep of deps) { if (pkg.categorizeDependency(dep.name) === "peerDependencies") { peers.add(dep.name); } } return peers; } async writeLinkedPackage(name, target, destination) { var _a, _b; let targetPkg = fs.readJsonSync(`${target}/package.json`); let peers = this.transitivePeers(target); if (peers.size === 0) { fs.ensureSymlinkSync(target, destination, "dir"); return; } await this.hardLinkContents(target, targetPkg, destination); let depsToLink = /* @__PURE__ */ new Set(); for (let section of ["dependencies", "peerDependencies"]) { if (targetPkg[section]) { for (let depName of Object.keys(targetPkg[section])) { if (peers.has(depName)) { continue; } depsToLink.add(depName); } } } let optionalPeers = []; for (let depName of Object.keys((_a = targetPkg["peerDependenciesMeta"]) != null ? _a : {})) { if (((_b = targetPkg["peerDependenciesMeta"][depName]) == null ? void 0 : _b.optional) === true) { optionalPeers.push(depName); } } for (let depName of depsToLink) { let depTarget = resolvePackagePath(depName, target, this.resolutionCache); if (!depTarget && optionalPeers.includes(depName)) { continue; } if (!depTarget) { throw new Error( `[FixturifyProject] package ${name} in ${target} depends on ${depName} but we could not resolve it` ); } await this.writeLinkedPackage(depName, path.dirname(depTarget), path.join(destination, "node_modules", depName)); } } async discoverWorkspaces() { for (let opts of this.dependencyLinks.values()) { if (!("baseDir" in opts)) { continue; } let dir = opts.baseDir; if (this.knownWorkspaces.has(dir)) { continue; } let top = await findWorkspaceDir(dir); if (top) { let packages = await findWorkspacePackages(top); for (let { dir: dir2 } of packages) { this.knownWorkspaces.set(dir2, true); } if (!this.knownWorkspaces.has(dir)) { this.knownWorkspaces.set(dir, false); } } else { this.knownWorkspaces.set(dir, false); } } } async publishedPackageContents(packageDir, pkgJSON) { if (this.knownWorkspaces.get(packageDir)) { return await packlist(packageDir, { packageJsonCache: { packageDir: pkgJSON } }); } return walkSync(packageDir, { directories: false, ignore: ["node_modules"] }); } async hardLinkContents(source, pkgJSON, destination) { fs.ensureDirSync(destination); for (let relativePath of await this.publishedPackageContents(source, pkgJSON)) { fs.ensureDirSync(path.dirname(path.join(destination, relativePath))); this.hardLinkFile(path.join(source, relativePath), path.join(destination, relativePath)); } } hardLinkFile(source, destination) { if (this.usingHardLinks) { try { fs.linkSync(source, destination); return; } catch (err) { if (err.code === "EEXIST") { debugger; } if (err.code === "EXDEV") { this.usingHardLinks = false; } } } fs.copyFileSync(source, destination, fs.constants.COPYFILE_FICLONE | fs.constants.COPYFILE_EXCL); } readSync(baseDir, opts) { const files = fixturify.readSync(baseDir, { ignore: (opts == null ? void 0 : opts.linkDeps) || (opts == null ? void 0 : opts.linkDevDeps) ? ["node_modules"] : [] }); this.pkg = deserializePackageJson(getFile(files, "package.json")); this.requestedRange = this.version; delete files["package.json"]; this.files = files; if ((opts == null ? void 0 : opts.linkDeps) || (opts == null ? void 0 : opts.linkDevDeps)) { if (this.pkg.dependencies) { for (let dep of Object.keys(this.pkg.dependencies)) { this.linkDependency(dep, { baseDir }); } } if (this.pkg.devDependencies && opts.linkDevDeps) { for (let dep of Object.keys(this.pkg.devDependencies)) { this.linkDevDependency(dep, { baseDir }); } } } else { const nodeModules = getFolder(files, "node_modules"); delete files["node_modules"]; keys(this.pkg.dependencies).forEach((dependency) => { this.addDependency( new this.constructor({ files: unwrapPackageName(nodeModules, dependency) }) ); }); keys(this.pkg.devDependencies).forEach((dependency) => { this.addDevDependency( new this.constructor({ files: unwrapPackageName(nodeModules, dependency) }) ); }); } } addDep(first, second, third, target, projectCallback) { let dep; if (first == null) { dep = new Project(); } else if (typeof first === "string") { let name = first; if (typeof second === "string") { let version = second; dep = new Project(name, version, third, projectCallback); } else { dep = new Project(name, second, projectCallback); } } else if ("isDependency" in first) { dep = first; } else { dep = new Project(first, projectCallback); } this[target][dep.name] = dep; this.dependencyLinks.delete(dep.name); this.linkIsDevDependency.delete(dep.name); if (isProjectCallback(projectCallback)) { projectCallback(dep); } return dep; } pkgJSONWithDeps(resolvedLinks) { let dependencies = this.depsToObject(this.dependencyProjects()); let devDependencies = this.depsToObject(this.devDependencyProjects()); for (let [name, { requestedRange }] of resolvedLinks) { if (requestedRange === void 0) { throw new Error( `No version found for package ${name}. All dependencies must have both a name and version in their package.json.` ); } if (this.linkIsDevDependency.has(name)) { devDependencies[name] = requestedRange; } else { dependencies[name] = requestedRange; } } return Object.assign(this.pkg, { dependencies, devDependencies }); } depsToObject(deps) { let obj = {}; deps.forEach((dep) => obj[dep.name] = dep.requestedRange); return obj; } }; function deserializePackageJson(serialized) { return JSON.parse(serialized); } function keys(object) { if (object !== null && (typeof object === "object" || Array.isArray(object))) { return Object.keys(object); } else { return []; } } function isProjectCallback(maybe) { return typeof maybe === "function"; } function getString(obj, propertyName, errorMessage) { const value = obj[propertyName]; if (typeof value === "string") { return value; } else { throw new TypeError(errorMessage || `expected 'string' but got '${typeof value}'`); } } function getFile(dir, fileName) { const value = dir[fileName]; if (typeof value === "string") { return value; } else if (typeof value === "object" && value !== null) { throw new TypeError(`Expected a file for name '${String(fileName)}' but got a 'Folder'`); } else { throw new TypeError(`Expected a file for name '${String(fileName)}' but got '${typeof value}'`); } } function getFolder(dir, fileName) { const value = dir[fileName]; if (isDirJSON(value)) { return value; } else if (typeof value === "string") { throw new TypeError(`Expected a file for name '${String(fileName)}' but got 'File'`); } else { throw new TypeError(`Expected a folder for name '${String(fileName)}' but got '${typeof value}'`); } } function isDirJSON(value) { return typeof value === "object" && value !== null; } function getPackageName(pkg) { return getString(pkg, "name", `package.json is missing a name.`); } function getPackageVersion(pkg) { return getString(pkg, "version", `${getPackageName(pkg)}'s package.json is missing a version.`); } function parseScoped(name) { let matched = name.match(/(@[^@\/]+)\/(.*)/); if (matched) { return { scope: matched[1], name: matched[2] }; } return null; } function unwrapPackageName(obj, packageName) { let scoped = parseScoped(packageName); if (scoped) { return getFolder(getFolder(obj, scoped.scope), scoped.name); } return getFolder(obj, packageName); } function isObject(e) { return e !== null && typeof e === "object" && !Array.isArray(e); } function isErrnoException(e) { return isObject(e) && "code" in e; } function readString(name) { try { return fs.readFileSync(name, "utf8"); } catch (e) { if (isErrnoException(e)) { if (e.code === "ENOENT" || e.code === "EISDIR") { return; } } throw e; } } function readdir(name) { try { return fs.readdirSync(name); } catch (e) { if (isErrnoException(e)) { if (e.code === "ENOENT" || e.code === "ENOTDIR") { return []; } } throw e; } } function readPackage(dir) { if (dir) { const fileName = path.join(dir, "package.json"); const content = readString(fileName); if (content) { return { pkg: deserializePackageJson(content), path: dir }; } } return; } function readPackages(modulesPath) { const pkgs = []; for (const name of readdir(modulesPath)) { if (name.startsWith("@")) { const scopePath = path.join(modulesPath, name); for (const name2 of readdir(scopePath)) { const pkg = readPackage(path.join(scopePath, name2)); if (pkg) pkgs.push(pkg); } } else { const pkg = readPackage(path.join(modulesPath, name)); if (pkg) pkgs.push(pkg); } } return pkgs; } export { Project }; //# sourceMappingURL=index.js.map