UNPKG

@softvisio/cli

Version:
1,536 lines (1,223 loc) 48.8 kB
import "#core/temporal"; import childProcess from "node:child_process"; import fs from "node:fs"; import path from "node:path"; import ansi from "#core/ansi"; import GitHubApi from "#core/api/github"; import { readConfig, readConfigSync, writeConfigSync } from "#core/config"; import env from "#core/env"; import File from "#core/file"; import FileTree from "#core/file-tree"; import { calculateMode, chmodSync, exists } from "#core/fs"; import { glob, globSync } from "#core/glob"; import GlobPatterns from "#core/glob/patterns"; import Locale from "#core/locale"; import SemanticVersion from "#core/semantic-version"; import stream from "#core/stream"; import { TarPackStream, TarUnpackStream } from "#core/stream/tar"; import Table from "#core/text/table"; import { TmpDir, TmpFile } from "#core/tmp"; import { confirm, objectIsEmpty, repeatAction, shellQuote } from "#core/utils"; import * as yaml from "#core/yaml"; import Git from "#lib/git"; import lintFile from "#lib/lint/file"; import Dependencies from "#lib/package/dependencies"; import Docs from "#lib/package/docs"; import Localization from "#lib/package/localization"; import Wiki from "#lib/package/wiki"; import { getCliConfig } from "#lib/utils"; export default class Package { #root; #rootPackage; #parentPackage; #isGitRoot; #isPackage; #isGitPackage; #config; #cliConfig; #version; #workspaces; #subPackages; #git; #wiki; #docs; #localization; #rootSlug; #parentSlug; #dependencies; constructor ( root, { rootPackage, parentPackage } = {} ) { this.#root = root; this.#rootPackage = rootPackage; this.#parentPackage = parentPackage; } // static static new ( dir ) { dir = env.findPackageRoot( dir ); if ( dir ) return new this( dir.replaceAll( "\\", "/" ) ); } static newGit ( dir ) { dir = env.findGitPackageRoot( dir ); if ( dir ) return new this( dir.replaceAll( "\\", "/" ) ); } static newGitRoot ( dir ) { dir = env.findGitRoot( dir ); if ( dir ) return new this( dir.replaceAll( "\\", "/" ) ); } // properties get root () { return this.#root; } get rootPackage () { return this.#rootPackage; } get parentPackage () { return this.#parentPackage; } get rootSlug () { if ( this.#rootSlug === undefined ) { if ( this.rootPackage ) { this.#rootSlug = path.relative( this.rootPackage.root, this.root ).replaceAll( "\\", "/" ); } else { this.#rootSlug = null; } } return this.#rootSlug; } get parentSlug () { if ( this.#parentSlug === undefined ) { if ( this.parentPackage ) { this.#parentSlug = path.relative( this.parentPackage.root, this.root ).replaceAll( "\\", "/" ); } else { this.#parentSlug = null; } } return this.#parentSlug; } get isGitRoot () { this.#isGitRoot ??= env.isGitRoot( this.root ); return this.#isGitRoot; } get isPackage () { this.#isPackage ??= env.isPackageRoot( this.root ); return this.#isPackage; } get isGitPackage () { this.#isGitPackage ??= env.isGitPackageRoot( this.root ); return this.#isGitPackage; } get hasDockerfile () { return fs.existsSync( this.root + "/dockerfile" ) || fs.existsSync( this.root + "/Dockerfile" ); } get git () { if ( !this.#git ) { this.#git = new Git( this.root ); } return this.#git; } get config () { if ( this.#config === undefined ) { this.#config = this.isPackage ? readConfigSync( this.root + "/package.json" ) : null; } return this.#config; } get cliConfig () { if ( this.#cliConfig === undefined ) { this.#cliConfig = getCliConfig( this.root + "/cli.config.yaml", { "validate": true, } ); } return this.#cliConfig; } get name () { return this.config?.name; } get version () { if ( this.#version === undefined ) { try { this.#version = SemanticVersion.new( this.config?.version ); } catch { this.#version = null; } } return this.#version; } get isPrivate () { return this.config ? Boolean( this.config.private ) : true; } get isReleaseEnabled () { if ( !this.isGitPackage ) return false; if ( !this.cliConfig ) return false; return this.cliConfig.release.enabled; } get workspaces () { BREAK: if ( !this.#workspaces ) { this.#workspaces = []; const workspaces = this.config.workspaces; if ( !workspaces ) break BREAK; for ( const pattern of workspaces ) { const root = this.#root + "/" + pattern; if ( env.isPackageRoot( root ) ) { this.#workspaces.push( new this.constructor( root, { "rootPackage": this.isGitPackage ? this : this.rootPackage, "parentPackage": this, } ) ); } } } return this.#workspaces; } get subPackages () { BREAK: if ( !this.#subPackages ) { this.#subPackages = []; const subPackages = this.cliConfig?.subPackages; if ( !subPackages ) break BREAK; for ( const pkg of globSync( subPackages, { "cwd": this.#root, "files": false, "directories": true, } ) ) { const root = this.#root + "/" + pkg; if ( env.isPackageRoot( root ) ) { const pkg = new this.constructor( root, { "rootPackage": this.isGitPackage ? this : this.rootPackage, "parentPackage": this, } ); this.#subPackages.push( pkg, ...pkg.subPackages ); } } } return this.#subPackages; } get wiki () { if ( !this.#wiki ) { this.#wiki = new Wiki( this ); } return this.#wiki; } get docs () { if ( !this.#docs ) { this.#docs = new Docs( this ); } return this.#docs; } get docsUrl () { if ( !this.git.upstream ) { return null; } else if ( this.cliConfig?.docs?.location ) { return this.git.upstream.docsUrl; } else { return this.git.upstream.readmeUrl; } } get npmUrl () { if ( !this.name ) return null; if ( this.isPrivate || !this.name ) return null; return `https://www.npmjs.com/package/${ this.name }`; } get localization () { this.#localization ??= new Localization( this ); return this.#localization; } get workspaceSlug () { env.loadUserEnv(); const workspace = process.env[ "SOFTVISIO_CLI_WORKSPACE_" + process.platform.toUpperCase() ]; if ( !workspace ) return null; return path.relative( workspace, this.root ).replaceAll( "\\", "/" ); } get isDependenciesLocked () { return this.hasPackageLock || this.hasNpmShrinkwrap; } get hasPackageLock () { return fs.existsSync( this.root + "/package-lock.json" ); } get hasNpmShrinkwrap () { return fs.existsSync( this.root + "/npm-shrinkwrap.json" ); } get dependencies () { if ( !this.#dependencies ) { this.#dependencies = new Dependencies( this.config ); } return this.#dependencies; } // public patchVersion ( version ) { const root = this.root; // update package.json const pkg = readConfigSync( root + "/package.json" ); pkg.version = version; writeConfigSync( root + "/package.json", pkg, { "readable": true } ); // update npm-shrinkwrap.json if ( fs.existsSync( root + "/npm-shrinkwrap.json" ) ) { const data = readConfigSync( root + "/npm-shrinkwrap.json" ); data.version = version; if ( data.packages && data.packages[ "" ] ) data.packages[ "" ].version = version; writeConfigSync( root + "/npm-shrinkwrap.json", data, { "readable": true } ); } // update package-lock.json if ( fs.existsSync( root + "/package-lock.json" ) ) { const data = readConfigSync( root + "/package-lock.json" ); data.version = version; if ( data.packages && data.packages[ "" ] ) data.packages[ "" ].version = version; writeConfigSync( root + "/package-lock.json", data, { "readable": true } ); } // update cordova config.xml if ( fs.existsSync( root + "/config.xml" ) ) { var xml = fs.readFileSync( root + "/config.xml", "utf8" ), replaced; xml = xml.replace( /(<widget[^>]+version=")\d+\.\d+\.\d+(")/, ( ...match ) => { replaced = true; return match[ 1 ] + version + match[ 2 ]; } ); if ( replaced ) fs.writeFileSync( root + "/config.xml", xml ); } this.#clearCache(); } async publishNpm ( latestTag, nextTag ) { if ( !this.name || this.isPrivate ) return; this.#clearCache(); var res; // pack package res = await repeatAction( async () => { const tmpDir = new TmpDir(), command = shellQuote( [ "npm", "pack", "--json", "--pack-destination", tmpDir.path, this.root ] ); console.log( "Packing:", command ); const res = childProcess.spawnSync( command, { "shell": true, "stdio": "pipe", } ); if ( res.status ) { console.log( res.stderr + "" ); console.log( "Unable to pack package" ); return result( 500 ); } const output = JSON.parse( res.stdout ), tmpFile = new TmpFile( { "extname": ".tgz", } ), executablesPatterns = this.cliConfig?.meta?.executables ? new GlobPatterns().add( this.cliConfig.meta.executables ) : null; // fix permissions await stream.promises.pipeline( // fs.createReadStream( tmpDir.path + "/" + output[ 0 ].filename ), new TarUnpackStream(), new TarPackStream( { "gzip": true, "onWriteEntry": writeEntry => { if ( executablesPatterns?.test( writeEntry.path.replace( /^package\//, "" ) ) ) { writeEntry.mode = calculateMode( "rwxr-xr-x" ); } else { writeEntry.mode = calculateMode( "rw-r--r--" ); } }, } ), fs.createWriteStream( tmpFile.path ) ); return result( 200, { "pack": tmpFile, } ); } ); if ( !res.ok ) return res; const pack = res.data.pack; // publish npm package const addTags = [ latestTag, nextTag ].filter( tag => tag ).map( tag => `"${ tag }"` ), args = [ "npm", "publish", "--access", "public" ]; if ( addTags.length ) { args.push( "--tag", addTags.shift() ); } args.push( pack.path ); const command = shellQuote( args ); res = await repeatAction( async () => { console.log( "Publishing:", command ); const res = childProcess.spawnSync( command, { "shell": true, "stdio": "pipe", } ); if ( res.status ) { console.log( res.stderr + "" ); console.log( "Unable to publish to the npm registry" ); return result( 500 ); } else { return result( 200 ); } } ); if ( !res.ok ) return res; // add additional tags if ( addTags.length ) { for ( const tag of addTags ) { const command = shellQuote( [ "npm", "dist-tag", "add", `${ this.name }@${ this.version }`, tag ] ); res = await repeatAction( async () => { console.log( "Adding npm tag:", command ); const res = childProcess.spawnSync( command, { "shell": true, "stdio": "pipe", } ); // error if ( res.status ) { console.log( res.stderr + "" ); console.log( `Unable to add tag "${ tag }"` ); return result( 500 ); } else { return result( 200 ); } } ); if ( !res.ok ) return res; } } return result( 200 ); } checkPreReleaseDependencies () { const preReleaseDependencies = this.dependencies.preReleaseNames; if ( preReleaseDependencies.size ) { return result( [ 500, `Package "${ this.name }" has pre-release dependencies: ${ [ ...preReleaseDependencies ] .sort() .map( name => `"${ name }"` ) .join( ", " ) }`, ] ); } else { return result( 200 ); } } async release ( { preReleaseTag, yes } = {} ) { const { "default": Release } = await import( "./package/release.js" ); return new Release( this, { preReleaseTag, yes, } ); } async updateMetadata ( { repository, dependabot, commit, log } = {} ) { var res, report = ""; // configure upstream repository if ( repository ) { res = await this.configureUpstreamRepository(); const reportText = "Configure upstream repository: " + ( res.ok ? ( res.data.updated ? ansi.ok( " Updated " ) : "Not modified" ) : ansi.error( " " + res.statusText + " " ) ); if ( log ) console.log( reportText ); report += reportText + "\n"; if ( !res.ok ) { return result( res, { "log": report, } ); } } // update metadata { res = await this.#updateMetadata( { dependabot, commit } ); const reportText = "Update metadata: " + ( res.ok ? ( res.data.updated ? ansi.ok( " Updated " ) : "Not modified" ) : ansi.error( " " + res.statusText + " " ) ); if ( log ) console.log( reportText ); report += reportText + "\n"; if ( !res.ok ) { return result( res, { "log": report, } ); } } return result( 200, { "log": report, } ); } async updateFilesMode () { if ( !this.cliConfig?.meta?.executables ) return result( 200 ); var res; const packagePatterns = new GlobPatterns().add( "**" ); for ( const pkg of this.subPackages ) { packagePatterns.add( "!" + pkg.parentSlug + "/**" ); } res = await this.git.exec( [ "ls-files", "--format", "%(objectmode) %(path)" ] ); if ( !res ) return res; const files = Object.fromEntries( res.data .split( "\n" ) .map( line => line.split( " ", 2 ).reverse() ) .filter( ( [ path, mode ] ) => path && packagePatterns.test( path ) ) .map( ( [ path, mode ] ) => [ path, mode.endsWith( "755" ) ] ) ); const executablePatterns = new GlobPatterns().add( this.cliConfig.meta.executables ); const setX = [], dropX = []; for ( const [ path, executable ] of Object.entries( files ) ) { if ( executablePatterns.test( path ) ) { if ( !executable ) setX.push( path ); } else { if ( executable ) dropX.push( path ); } } if ( setX.length ) { res = await this.git.exec( [ "update-index", "--chmod=+x", ...setX ] ); if ( !res.ok ) return res; if ( process.platform !== "win32" ) { for ( const file of setX ) { chmodSync( this.root + "/" + file, "+x" ); } } } if ( dropX.length ) { res = await this.git.exec( [ "update-index", "--chmod=-x", ...dropX ] ); if ( !res.ok ) return res; if ( process.platform !== "win32" ) { for ( const file of dropX ) { chmodSync( this.root + "/" + file, "-x" ); } } } return result( 200 ); } runCommand ( command, ...args ) { const res = childProcess.spawnSync( shellQuote( [ command, ...args ] ), { "cwd": this.root, "stdio": "inherit", "shell": true, } ); if ( res.status ) { return result( 500 ); } else { return result( 200 ); } } runScript ( script, argv ) { if ( !this.config.scripts?.[ script ] ) return result( 200 ); if ( argv?.length ) { argv = [ "--", ...argv ]; } else { argv = []; } const res = childProcess.spawnSync( shellQuote( [ "npm", "run", script, ...argv ] ), { "cwd": this.root, "stdio": "inherit", "shell": true, } ); if ( res.status ) { return result( 500 ); } else { return result( 200 ); } } async getOutdatedDependencies ( { all } = {} ) { if ( !this.dependencies.hasDependencies ) return result( 200 ); return new Promise( resolve => { childProcess.exec( "npm outdated --json" + ( all ? " --all" : "" ), { "cwd": this.root, "maxBuffer": Infinity, }, ( error, stdout ) => { try { const dependencies = JSON.parse( stdout ); resolve( result( 200, dependencies ) ); } catch { resolve( result( 500 ) ); } } ); } ); } async updateDependencies ( { all, outdated, linked, missing, install, reinstall, commit, quiet, confirmInstall, outdatedDependencies, cache = {} } = {} ) { if ( !this.dependencies.hasDependencies ) return result( 200 ); var res; // get outdated dependencies if ( !outdatedDependencies ) { res = await this.getOutdatedDependencies( { all } ); if ( !res.ok ) return res; outdatedDependencies = res.data; } outdatedDependencies ||= {}; // add linked deps if ( linked ) { const linkedDependencies = await this.#getLinkedDependencies(); for ( const dependency of linkedDependencies.values() ) { if ( !outdatedDependencies[ dependency.name ] ) { outdatedDependencies[ dependency.name ] = []; } else if ( !Array.isArray( outdatedDependencies[ dependency.name ] ) ) { outdatedDependencies[ dependency.name ] = [ outdatedDependencies[ dependency.name ] ]; } outdatedDependencies[ dependency.name ].push( { "location": "-", "linked": dependency.link || true, } ); } } var hasUpdates; const updateDependencies = []; for ( const name in outdatedDependencies ) { let specs; if ( Array.isArray( outdatedDependencies[ name ] ) ) { specs = outdatedDependencies[ name ]; } else { specs = [ outdatedDependencies[ name ] ]; } const index = {}, locations = {}; for ( const spec of specs ) { // group specs by id const id = `${ spec.location }/${ spec.current }/${ spec.wanted }/${ spec.latest }`; index[ id ] ??= { name, "current": spec.current, "wanted": spec.wanted, "latest": spec.latest, "location": spec.location, "dependent": new Set(), "linked": spec.linked, "topLevel": this.dependencies.has( name ) && spec.location === path.join( this.root, "node_modules", name ), }; index[ id ].dependent.add( spec.dependent ); // detect updatable by location if ( spec.current === spec.wanted ) { locations[ spec.location ] = false; } else if ( locations[ spec.location ] !== false ) { locations[ spec.location ] = true; } } specs = Object.values( index ).map( spec => { spec.dependent = [ ...spec.dependent ].sort().join( ", " ); return spec; } ); for ( const spec of specs ) { // outdated dependency spec.outdated = spec.wanted !== spec.latest && new SemanticVersion( spec.latest ).gt( spec.wanted ); // include installed, updatable deps by default let include; if ( spec.linked ) { spec.updatable = true; include = true; } else { spec.updatable = locations[ spec.location ]; if ( spec.updatable && spec.current ) { include = true; } } // include outdated deps if ( outdated && spec.outdated && spec.topLevel ) { include = true; } // include linked deps if ( spec.linked ) { include = true; } // include misseing deps if ( missing && !spec.current ) { include = true; } if ( !include ) continue; // updatable dependency if ( spec.updatable ) { hasUpdates = true; } updateDependencies.push( { name, ...spec, } ); } } install = install && ( hasUpdates || reinstall ); // print report if ( install || updateDependencies.length ) { if ( !cache.newLine ) { cache.newLine = true; } else { console.log(); } console.log( "Package:", ansi.hl( this.workspaceSlug ) ); if ( updateDependencies.length && !quiet ) { new Table( { "columns": { "name": { "title": ansi.hl( "DEPENDENCY" ), "headerAlign": "center", "headerValign": "end", }, "dependent": { "title": ansi.hl( "DEPENDENT" ), "headerAlign": "center", "headerValign": "end", }, "current": { "title": ansi.hl( "INSTALLED" ), "headerAlign": "center", "headerValign": "end", "align": "end", "width": 20, "format": value => { return value ? ` ${ value } ` : " - "; }, }, "wanted": { "title": ansi.hl( "WANTED" ), "headerAlign": "center", "headerValign": "end", "align": "end", "width": 20, "format": ( value, row ) => { if ( !value ) { return " - "; } else if ( row.updatable ) { return ansi.ok( ` ${ value } ` ); } else { return ` ${ value } `; } }, }, "latest": { "title": ansi.hl( "LATEST" ), "headerAlign": "center", "headerValign": "end", "align": "end", "width": 20, "format": ( value, row ) => { if ( !value ) { if ( row.linked === true ) { return ansi.error( " MISSING " ); } else if ( row.linked ) { return ansi.error( " LINKED " ); } else { return ansi.error( " NOT FOUND " ); } } else if ( row.outdated ) { return ansi.error( ` ${ value } ` ); } else { return ` ${ value } `; } }, }, }, } ) .pipe( process.stdout ) .add( ...updateDependencies ) .end(); } } // nothing to update if ( !install ) return result( 200 ); // confirm update if ( hasUpdates && confirmInstall ) { res = await confirm( "Update dependencies?", [ "[yes]", "no" ] ); if ( res !== "yes" ) return result( 200 ); } // perform update process.stdout.write( "Updating dependencies ... " ); res = childProcess.spawnSync( "npm update --json", { "cwd": this.root, "stdio": [ "ignore", "pipe", "ignore" ], "shell": true, } ); // update failed if ( res.status ) { console.log( ansi.error( " ERROR " ) ); const output = JSON.parse( res.stdout ); console.log( output.error.summary ); console.log( output.error.detail ); return result( 500 ); } else { console.log( ansi.ok( " OK " ) ); } // commit and push if ( commit ) { // get working tree status res = await this.git.getWorkingTreeStatus(); if ( !res.ok ) return res; const commitFiles = []; // working tree is dirty if ( res.data.isDirty ) { for ( const lockFile of [ "package-lock.json", "npm-shrinkwrap.json" ] ) { const lockFilePath = this.rootSlug ? this.rootSlug + "/" + lockFile : lockFile; if ( res.data.files[ lockFilePath ] ) commitFiles.push( lockFile ); } } // dependencies locks was not updated if ( !commitFiles.length ) return result( 200 ); process.stdout.write( "Commit and push ... " ); // add changes res = await repeatAction( async () => { const res = await this.git.exec( [ "add", ...commitFiles ] ); if ( !res.ok ) console.log( ansi.error( res + "" ) ); return res; } ); if ( !res.ok ) return res; // commit changes res = await repeatAction( async () => { const res = await this.git.exec( [ "commit", "-m", "chore(deps): update locked dependencies", ...commitFiles ] ); if ( !res.ok ) console.log( ansi.error( res + "" ) ); return res; } ); if ( !res.ok ) return res; // push changes res = await repeatAction( async () => { const res = await this.git.exec( [ "push" ] ); if ( !res.ok ) console.log( ansi.error( res + "" ) ); return res; } ); if ( !res.ok ) return res; console.log( ansi.ok( " OK " ) ); } return result( 200 ); } test ( { log } = {} ) { var res; if ( !this.config.scripts?.test ) { res = result( [ 200, "No tests to run" ] ); } else { res = childProcess.spawnSync( "npm", [ "test" ], { "cwd": this.root, "stdio": "pipe", } ); if ( res.status ) { console.log( res.stderr + "" ); res = result( [ 500, "Tests failed" ] ); } else { res = result( 200 ); } } if ( log ) { console.log( `Tests result "${ this.workspaceSlug }":`, res + "" ); } return res; } async configureUpstreamRepository () { const upstream = this.git.upstream; if ( !upstream.isGitHub ) return result( [ 400, "Repository upstream is not GitHub" ] ); env.loadUserEnv(); if ( !process.env.GITHUB_TOKEN ) return result( [ 400, "GitHub token is not provided" ] ); const gitHubApi = new GitHubApi( process.env.GITHUB_TOKEN ), repositorySettings = this.cliConfig.meta.repository, homepage = ( this.docs.isEnabled && upstream.docsUrl ) || upstream.homeUrl; var res, data, updated = false; // get repository settings res = await gitHubApi.getRepository( upstream.repositorySlug ); if ( !res.ok ) return res; const currentData = res.data; // description if ( this.config.description && this.config.description !== currentData.description ) { data ??= {}; data.description = this.config.description; } // homepage if ( currentData.homepage !== homepage ) { data ??= {}; data.homepage = homepage; } // private if ( repositorySettings.private !== null && repositorySettings.private !== currentData.private ) { data ??= {}; data.private = repositorySettings.private; currentData.private = repositorySettings.private; } // visibility if ( repositorySettings.visibility !== null && repositorySettings.visibility !== currentData.visibility ) { data ??= {}; data.visibility = repositorySettings.visibility; } // issues if ( repositorySettings.hasIssues !== null && repositorySettings.hasIssues !== currentData.has_issues ) { data ??= {}; data.has_issues = repositorySettings.hasIssues; } // projects if ( repositorySettings.hasProjects !== null && repositorySettings.hasProjects !== currentData.has_projects ) { data ??= {}; data.has_projects = repositorySettings.hasProjects; } // wiki if ( repositorySettings.hasWiki !== null && repositorySettings.hasWiki !== currentData.has_wiki ) { data ??= {}; data.has_wiki = repositorySettings.hasWiki; } // discussions if ( repositorySettings.hasDiscussions !== null && repositorySettings.hasDiscussions !== currentData.has_discussions ) { data ??= {}; data.has_discussions = repositorySettings.hasDiscussions; } // default branch if ( repositorySettings.defaultBranch !== null && repositorySettings.defaultBranch !== currentData.default_branch ) { data ??= {}; data.default_branch = repositorySettings.defaultBranch; } // allow forking if ( repositorySettings.allowForking !== null && repositorySettings.allowForking !== currentData.allow_forking ) { data ??= {}; data.allow_forking = repositorySettings.allowForking; } // web commit signoff required if ( repositorySettings.webCommitSignoffRequired !== null && repositorySettings.webCommitSignoffRequired !== currentData.web_commit_signoff_required ) { data ??= {}; data.web_commit_signoff_required = repositorySettings.webCommitSignoffRequired; } // security and analysis if ( currentData.security_and_analysis ) { // secret scanning if ( repositorySettings.secretScanning !== null && repositorySettings.secretScanning !== currentData.security_and_analysis.secret_scanning.status ) { data ??= {}; data.security_and_analysis ??= {}; data.security_and_analysis.secret_scanning = { "status": repositorySettings.secretScanning, }; } // secret scanning push protection if ( repositorySettings.secretScanningPushProtection !== null && repositorySettings.secretScanningPushProtection !== currentData.security_and_analysis.secret_scanning_push_protection.status ) { data ??= {}; data.security_and_analysis ??= {}; data.security_and_analysis.secret_scanning_push_protection = { "status": repositorySettings.secretScanningPushProtection, }; } } if ( data ) { updated = true; res = await gitHubApi.updateRepository( upstream.repositorySlug, data ); if ( !res.ok ) return res; } // vulnerability alerts if ( repositorySettings.vulnerabilityAlerts != null ) { res = await gitHubApi.getVulnerabilityAlertsEnabled( upstream.repositorySlug ); if ( !res.ok ) return res; if ( repositorySettings.vulnerabilityAlerts !== res.data.enabled ) { res = await gitHubApi.setVulnerabilityAlertsEnabled( upstream.repositorySlug, repositorySettings.vulnerabilityAlerts ); if ( !res.ok ) return res; updated = true; } } // dependabot Security Updates if ( repositorySettings.dependabotsecurityupdates != null ) { res = await gitHubApi.getDependabotsecurityupdatesEnabled( upstream.repositorySlug ); if ( !res.ok ) return res; if ( repositorySettings.dependabotsecurityupdates !== res.data.enabled ) { res = await gitHubApi.setDependabotsecurityupdateEnabled( upstream.repositorySlug, repositorySettings.dependabotsecurityupdates ); if ( !res.ok ) return res; updated = true; } } // private vulnerability reporting if ( repositorySettings.privateVulnerabilityReporting != null && !currentData.private ) { res = await gitHubApi.getPrivateVulnerabilityReportingEnabled( upstream.repositorySlug ); if ( !res.ok ) return res; if ( repositorySettings.privateVulnerabilityReporting !== res.data.enabled ) { res = await gitHubApi.setPrivateVulnerabilityReportingEnabled( upstream.repositorySlug, repositorySettings.privateVulnerabilityReporting ); if ( !res.ok ) return res; updated = true; } } return result( 200, { updated, } ); } // private #clearCache () { this.#config = undefined; this.#cliConfig = undefined; this.#version = undefined; this.#workspaces = undefined; this.#subPackages = undefined; this.#dependencies = undefined; } async #updateMetadata ( { dependabot, commit } = {} ) { var res, updated; // get git status res = await this.git.getWorkingTreeStatus(); if ( !res.ok ) return res; // package is dirty if ( res.data.isDirty ) return result( [ 500, "Package has uncommited changes" ] ); const upstream = this.git.upstream, packages = [ this, ...this.subPackages ], fileTree = new FileTree(); for ( const pkg of packages ) { const config = await readConfig( pkg.root + "/package.json" ); // bugs config.bugs = { "url": upstream.issuesUrl, "email": process.env.META_BUGS_EMAIL || process.env.META_AUTHOR, }; // repository config.repository = { "type": "git", "url": "git+" + upstream.httpsCloneUrl, }; if ( pkg.rootSlug ) { config.repository.directory = pkg.rootSlug; } else { delete config.repository.directory; } // homepage if ( pkg.cliConfig ) { config.homepage = pkg.cliConfig.meta.homepage || ( this.docs.isEnabled && upstream.docsUrl ) || upstream.homeUrl; } else { config.homepage ||= upstream.homeUrl; } // license if ( pkg.cliConfig ) { config.license = pkg.cliConfig.meta.license || config.private ? process.env.META_LICENSE_PRIVATE : process.env.META_LICENSE_PUBLIC; } // author if ( pkg.cliConfig ) { config.author = pkg.cliConfig.meta.author || process.env.META_AUTHOR; } // scripts if ( pkg.cliConfig ) { // "test" script if ( ( await glob( "tests/**/*.test.js", { "cwd": pkg.root } ) ).length ) { config.scripts ??= {}; config.scripts.test = "node --test tests/**/*.test.js"; } else { delete config.scripts?.test; } // dependencies const dependencies = new Dependencies( config ); dependencies.fix(); if ( config.scripts && objectIsEmpty( config.scripts ) ) { delete config.scripts; } } fileTree.add( { "path": ( pkg.rootSlug || "" ) + "/package.json", "buffer": JSON.stringify( config, null, 4 ) + "\n", } ); // chmod res = await pkg.updateFilesMode(); if ( !res.ok ) return res; } // dependabot if ( dependabot ) { res = await this.#updateDependabotConfig(); if ( !res.ok ) return res; if ( res.data ) { fileTree.add( res.data ); } } // lint for ( const file of fileTree ) { const res = await lintFile( new File( { "path": path.join( this.root, file.path ), "buffer": await file.text(), } ) ); if ( !res.ok ) return res; fileTree.add( new File( { "path": file.path, "buffer": res.data, } ) ); } // write file tree await fileTree.write( this.root ); // get git status res = await this.git.getWorkingTreeStatus(); if ( !res.ok ) return res; updated = res.data.isDirty; if ( updated ) { // commit and push if ( commit ) { // add changes res = await this.git.exec( [ "add", "." ] ); if ( !res.ok ) return res; // commit changes res = await this.git.exec( [ "commit", "-m", "chore(metadata): update package metadata" ] ); if ( !res.ok ) return res; // push res = await this.git.exec( [ "push" ] ); if ( !res.ok ) return res; } } return result( 200, { updated, } ); } async #updateDependabotConfig () { const upstream = this.git.upstream; if ( !upstream.isGitHub ) return result( 200 ); var filename, config, updates = new Map(); if ( await exists( this.root + "/.github/dependabot.yaml" ) ) { filename = "dependabot.yaml"; config = await readConfig( this.root + "/.github/dependabot.yaml" ); } else if ( await exists( this.root + "/.github/dependabot.yml" ) ) { filename = "dependabot.yml"; config = await readConfig( this.root + "/.github/dependabot.yml" ); } else { filename = "dependabot.yaml"; } if ( config ) { for ( const update of config.updates || [] ) { if ( update[ "package-ecosystem" ] === "npm" ) continue; if ( update[ "package-ecosystem" ] === "docker" ) continue; if ( update[ "package-ecosystem" ] === "github-actions" ) continue; updates.set( update[ "package-ecosystem" ], update ); } } // npm if ( this.cliConfig?.meta.dependabot.npm?.interval ) { const directories = []; for ( const pkg of [ this, ...this.subPackages ] ) { if ( pkg.cliConfig?.meta.dependabot.npm ) { if ( pkg.config.dependencies || pkg.config.devDependencies || pkg.config.peerDependencies ) { directories.push( "/" + ( pkg.rootSlug || "" ) ); } } } if ( directories.length ) { updates.set( "npm", { "package-ecosystem": "npm", "directories": directories.sort(), "schedule": { "interval": this.cliConfig.meta.dependabot.npm.interval, "day": this.cliConfig.meta.dependabot.npm.day.toLowerCase(), "time": new Locale().formatDate( Temporal.PlainTime.from( this.cliConfig.meta.dependabot.npm.time ), "timeStyle:short" ), "timezone": this.cliConfig.meta.dependabot.npm.timezone, }, "open-pull-requests-limit": this.cliConfig.meta.dependabot.npm[ "open-pull-requests-limit" ], } ); } } // docker if ( this.cliConfig?.meta.dependabot.docker?.interval && this.hasDockerfile ) { updates.set( "docker", { "package-ecosystem": "docker", "directories": [ "/" ], "schedule": { "interval": this.cliConfig.meta.dependabot.docker.interval, "day": this.cliConfig.meta.dependabot.docker.day.toLowerCase(), "time": new Locale().formatDate( Temporal.PlainTime.from( this.cliConfig.meta.dependabot.docker.time ), "timeStyle:short" ), "timezone": this.cliConfig.meta.dependabot.docker.timezone, }, "open-pull-requests-limit": this.cliConfig.meta.dependabot.docker[ "open-pull-requests-limit" ], } ); } // github-actions if ( this.cliConfig?.meta.dependabot[ "github-actions" ]?.interval && ( await glob( ".github/workflows/*.*", { "cwd": this.root } ) ).length ) { updates.set( "github-actions", { "package-ecosystem": "github-actions", "directories": [ "/" ], "schedule": { "interval": this.cliConfig.meta.dependabot[ "github-actions" ].interval, "day": this.cliConfig.meta.dependabot[ "github-actions" ].day.toLowerCase(), "time": new Locale().formatDate( Temporal.PlainTime.from( this.cliConfig.meta.dependabot[ "github-actions" ].time ), "timeStyle:short" ), "timezone": this.cliConfig.meta.dependabot[ "github-actions" ].timezone, }, "open-pull-requests-limit": this.cliConfig.meta.dependabot[ "github-actions" ][ "open-pull-requests-limit" ], } ); } if ( !updates.size ) { if ( await exists( this.root + "/.github" ) ) { await fs.promises.rm( this.root + "/.github/" + filename, { "force": true, } ); const files = await fs.promises.readdir( this.root + "/.github" ); if ( !files.length ) { await fs.promises.rm( this.root + "/.github", { "recursive": true, } ); } } return result( 200 ); } config = { "version": 2, "updates": [], }; for ( const update of [ ...updates.keys() ].sort() ) { config.updates.push( updates.get( update ) ); } return result( 200, new File( { "path": "/.github/" + filename, "buffer": yaml.toYaml( config ), } ) ); } async #getLinkedDependencies () { const dependencies = new Map(); await Promise.all( [ ...new Set( [ ...this.dependencies.names, ...( await glob( [ "*", "@*/*" ], { "cwd": this.root + "/node_modules", "files": false, "directories": true, } ) ), ] ), ].map( name => { const location = path.join( this.root, "node_modules", name ); return fs.promises .readlink( location ) .then( link => dependencies.set( name, { name, location, link, } ) ) .catch( e => { // not exists if ( e.code === "ENOENT" ) { dependencies.set( name, { name, location, "link": null, } ); } // other error else if ( e.code !== "EINVAL" ) { console.log( e ); } } ); } ) ); return dependencies; } }