| /** |
| * @license |
| * Copyright (C) 2020 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| import {PackageName, PackageVersion, DirPath, FilePath} from "./base-types"; |
| import {fail} from "./utils"; |
| import * as path from "path"; |
| import * as fs from "fs"; |
| |
| /** |
| * Describe one installed package from node_modules |
| */ |
| export interface InstalledPackage { |
| /** Package name (it is calculated based on path to module) */ |
| name: PackageName; |
| /** Package version from package.json */ |
| version: PackageVersion; |
| /** |
| * Path to the top-level package directory, where package.json is placed |
| * This path is relative to process working directory (because bazel pass all paths relative to it) |
| */ |
| rootPath: DirPath; |
| /** All files in package. Path is relative to rootPath */ |
| files: FilePath[]; |
| } |
| |
| /** |
| * Calculates all installed packages from a list of files |
| * It is expected, that the addPackageJson method is called first for |
| * all package.json files first and then the addFile method is called for all files (including package.json) |
| */ |
| export class InstalledPackagesBuilder { |
| private readonly rootPathToPackageMap: Map<DirPath, InstalledPackage> = new Map(); |
| |
| public constructor(private readonly nonPackages: Set<string>) {} |
| |
| public addPackageJson(packageJsonPath: string) { |
| const pack = this.createInstalledPackage(packageJsonPath); |
| if (!pack) return; |
| this.rootPathToPackageMap.set(pack.rootPath, pack); |
| } |
| |
| public addFile(file: string) { |
| const pack = file.includes("@file+") |
| ? this.findLocalPackageForFile(file) |
| : this.findPackageForFile(file); |
| |
| pack.files.push(path.relative(pack.rootPath, file)); |
| } |
| |
| /** |
| * Create new InstalledPackage. |
| * The name of a package is a relative path to the closest node_modules parent. |
| * For example for the packageJsonFile='/a/node_modules/b/node_modules/d/e/package.json' |
| * the package name is 'd/e' |
| */ |
| private createInstalledPackage(packageJsonFile: string): InstalledPackage | undefined { |
| const nameParts: Array<string> = []; |
| const rootPath = path.dirname(packageJsonFile); |
| let currentDir = rootPath; |
| while (currentDir !== "") { |
| const partName = path.basename(currentDir); |
| if (partName === "node_modules") { |
| const packageName = nameParts.reverse().join("/"); |
| const version = JSON.parse( |
| fs.readFileSync(packageJsonFile, {encoding: "utf-8"}) |
| )["version"]; |
| if (!version) { |
| if (this.nonPackages.has(packageName)) { |
| return undefined; |
| } |
| fail(`Can't get version for ${packageJsonFile}`); |
| } |
| return { |
| name: packageName, |
| rootPath: rootPath, |
| version: version, |
| files: [], |
| }; |
| } |
| nameParts.push(partName); |
| currentDir = path.dirname(currentDir); |
| } |
| fail(`Can't create package info for '${packageJsonFile}'`); |
| } |
| |
| private findPackageForFile(filePath: FilePath): InstalledPackage { |
| let currentDir = path.dirname(filePath); |
| |
| while (true) { |
| if (this.rootPathToPackageMap.has(currentDir)) { |
| return this.rootPathToPackageMap.get(currentDir)!; |
| } |
| |
| const nextDir = path.dirname(currentDir); |
| if (nextDir === currentDir) { |
| break; |
| } |
| currentDir = nextDir; |
| } |
| |
| fail(`Can't find package for '${filePath}'`); |
| } |
| |
| private findLocalPackageForFile(filePath: FilePath): InstalledPackage { |
| let currentDir = path.dirname(filePath); |
| currentDir = this.getPackagePathRelativeToNodeModules(currentDir); |
| |
| const parts = currentDir.split("/"); |
| const pack = |
| parts[0].startsWith("@") && parts.length > 1 |
| ? `${parts[0]}/${parts[1]}` |
| : parts[0]; |
| |
| if (this.rootPathToPackageMap.has(pack)) { |
| return this.rootPathToPackageMap.get(pack)!; |
| } |
| |
| const packIndex = filePath.lastIndexOf(pack); |
| if (packIndex === -1) { |
| fail(`Can't determine package root for '${filePath}'`); |
| } |
| |
| const installedPack: InstalledPackage = { |
| name: pack, |
| version: "Snapshot", |
| rootPath: filePath.substring(0, packIndex + pack.length), |
| files: [], |
| }; |
| this.rootPathToPackageMap.set(pack, installedPack); |
| return installedPack; |
| } |
| |
| public build(): InstalledPackage[] { |
| return [...this.rootPathToPackageMap.values()]; |
| } |
| |
| private getPackagePathRelativeToNodeModules(filePath: string): string { |
| const nodeModulesPathPart = "/node_modules/"; |
| const index = filePath.lastIndexOf(nodeModulesPathPart); |
| if (index === -1) { |
| fail(`Path does not contain '${nodeModulesPathPart}': '${filePath}'`); |
| } |
| return filePath.substring(index + nodeModulesPathPart.length); |
| } |
| } |