UNPKG

@google/aside

Version:

Apps Script IDE framework

210 lines (209 loc) 7.43 kB
/** * Copyright 2023 Google LLC * * 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 spawn from 'cross-spawn'; import fs from 'fs-extra'; import writeFileAtomic from 'write-file-atomic'; import { compare } from './compare.js'; /** The default package.json path in the current working directory. */ export const DEFAULT_PACKAGE_JSON_PATH = './package.json'; const DEFAULT_PACKAGE_JSON_CONTENT = { name: '', version: '0.0.0', description: '', main: 'build/index.js', license: 'Apache-2.0', keywords: [], scripts: {}, engines: { node: '>=22', }, }; /** * Reformats an arbitrary string into a lower-case-dashed package name. * @param {string} name the project name * @returns {string} the reformatted package name */ function toPackageName(name) { return name ?.replace(/([a-z])([A-Z])/g, '$1-$2') .replace(/[\s_]+/g, '-') .toLowerCase(); } /** * A collection of utilities for interacting with package.json files. */ export class PackageHelper { constructor(content = {}, path = DEFAULT_PACKAGE_JSON_PATH) { this.content = content; this.path = path; } /** * Returns a snapshot of the currently loaded package.json. * @returns {PackageJson} the package.json content */ getContent() { return JSON.parse(JSON.stringify(this.content)); } /** * Returns the package.json's package name * @returns {string} the name of the current package */ getName() { return this.content.name; } /** * Returns a snapshot of the package.json's script section. * @returns {PackageJson.Scripts} the scripts of the current package */ getScripts() { return this.getContent().scripts ?? {}; } /** * Returns a snapshot of the package.json's dependency section. * * Specify the includeDevDependencies flag in order to retrieve a merge of * the dependencies and development dependencies. * * @param {boolean} [includeDevDependencies=false] whether to include * devDependencies * @returns {PackageJson.Dependency} the dependencies of the current package */ getDependencies(includeDevDependencies = false) { const content = this.getContent(); const dependencies = content.dependencies ?? {}; if (includeDevDependencies) { return { ...dependencies, ...content.devDependencies }; } return dependencies; } /** * Returns a snapshot of the package.json's dependency package names. * * Specify the includeDevDependencies flag in order to retrieve a merge of * the dependency and development dependency package names. * * @param {boolean} [includeDevDependencies=false] whether to include * devDependencies * @returns {string[]} the dependency package names */ getDependencyPackages(includeDevDependencies = false) { return Object.keys(this.getDependencies(includeDevDependencies)); } /** * Updates the name of the current package. * * This function reformats the input name into a valid package name (i.e. * lower-case-dashed format) and returns the valid package name. * * @param {string} name the new name of the package * @returns {string} the reformatted package name */ updateName(name) { return (this.content.name = toPackageName(name)); } /** * Updates a single script entry in the current package. * * @param {string} name the name of the script. * @param {string} script the script content. * @returns {PackageJson.Scripts} a snapshot of the updated scripts section. */ updateScript(name, script) { if (!this.content.scripts) { this.content.scripts = {}; } this.content.scripts = { ...this.getScripts(), [name]: script }; return this.getScripts(); } /** * Installs a list of packages at their current version. * * Note this function computes the missing packages and only installs those. * The function returns an install result containing information, which * packages were requested, which ones already existed and which ones were * installed. * * @param {string[]} packages the packages to install. * @returns {PackageInstallResult} information about the installation. */ installPackages(packages) { const packagesToInstall = compare(this.getDependencyPackages(true), packages).right; if (packagesToInstall.length === 0) { return { requested: packages, resolved: packages, installed: [], }; } const executionResult = spawn.sync('npm', ['install', '--ignore-scripts', '--silent'].concat(packagesToInstall), { encoding: 'utf-8' }); if (executionResult.stderr) { throw new Error(executionResult.stderr); } // sync with new saved dependencies after install const packageJsonOnDisk = PackageHelper.load(this.path); if (!packageJsonOnDisk) { throw new Error('Cannot find package.json'); } const packageDiff = compare(this.getDependencyPackages(true), packageJsonOnDisk.getDependencyPackages(true)); this.content = packageJsonOnDisk.getContent(); return { requested: packages, resolved: packageDiff.both, installed: packageDiff.right, }; } /** * Writes the package.json to disk. * @returns {Promise<PackageHelper>} this package helper. */ async save() { await writeFileAtomic(this.path, `${JSON.stringify(this.content, null, ' ')}\n`); return this; } /** * Loads the contents of a package.json from the current directory. * @returns {PackageHelper|undefined} a package helper with the current package.json * information. */ static load(path = DEFAULT_PACKAGE_JSON_PATH) { try { return new PackageHelper(fs.readJsonSync(path), path); } catch (e) { if (e instanceof Error && 'code' in e && e.code === 'ENOENT') { return undefined; } else { // unexpected error throw e; } } } /** * Initializes a package.json with default values. * @param {string} name the package name * @param {string} [path] optionally, a path different from the default * package.json path. * @returns {PackageHelper} a package helper with the current package.json * information. */ static init(name, path = DEFAULT_PACKAGE_JSON_PATH) { return new PackageHelper({ ...DEFAULT_PACKAGE_JSON_CONTENT, name: toPackageName(name), }, path); } }