@bscotch/trebuchet
Advanced tools
Comparing version 2.1.3 to 2.4.0
#!/usr/bin/env node | ||
import { projectCreateSchema } from '@bscotch/project'; | ||
import { keysOf, pick } from '@bscotch/utility'; | ||
import { keysOf, pick, pointable } from '@bscotch/utility'; | ||
import { ok } from 'assert'; | ||
import JSON5 from 'json5'; | ||
import { stringify as yamlStringify } from 'yaml'; | ||
@@ -9,3 +10,3 @@ import { workspaceExternalDepsSchema, workspaceImportSchema, workspaceListSchema, workspacePublishSchema, } from '../lib/workspace.js'; | ||
import { TrebCli } from './treb.lib.js'; | ||
import { trebuchetExtractSchema, trebuchetFixSchema, trebuchetMoveFilesSchema, trebuchetMoveSchema, trebuchetProjectOption, trebuchetRenameSchema, trebuchetTestSchema, } from './treb.schema.js'; | ||
import { projectOptions, trebuchetExtractSchema, trebuchetFixSchema, trebuchetMoveFilesSchema, trebuchetMoveSchema, trebuchetProjectOption, trebuchetRenameSchema, trebuchetTestSchema, updateJsonOptionsSchema, } from './treb.schema.js'; | ||
const treb = new TrebCli({ | ||
@@ -19,3 +20,6 @@ name: 'treb', | ||
}) | ||
.addCommand('publish', 'Publish public, changed projects. Automatically fixes issues, bumps the version, and builds the changelog prior to publishing.', workspacePublishSchema, async (workspace, _, options) => { | ||
.addCommand('version', 'Bump the versions of changed projects. Uses Git tags to determine which projects have changed, bumps dependents of changed projects. (TODO: write changelogs.)', workspacePublishSchema, async (workspace, _, options) => { | ||
await workspace.versionProjects(options); | ||
}) | ||
.addCommand('publish', 'Publish projects that have had their versions bumped compared to the latest published version.', workspacePublishSchema, async (workspace, _, options) => { | ||
await workspace.publishProjects(options); | ||
@@ -35,3 +39,3 @@ }) | ||
ok(project, `Project not found`); | ||
await workspace.extractProject(project, options); | ||
await workspace.extractNewProject(project, options); | ||
}) | ||
@@ -95,2 +99,20 @@ .addCommand('import', 'Import an external project into the workspace.', workspaceImportSchema, async (workspace, project, options) => { | ||
}) | ||
.addCommand('update-json', 'Update a JSON or YAML file. Useful for enforcing a common field value in configuration files.', projectOptions(updateJsonOptionsSchema), async (_, project, options) => { | ||
const file = await project.dir.findChild(options.file); | ||
if (!file) { | ||
console.warn(`File ${options.file} not found in project ${project.name}`); | ||
return; | ||
} | ||
const json = await file.read(); | ||
let value = options.value; | ||
try { | ||
value = JSON5.parse(value); | ||
} | ||
catch { | ||
value = JSON5.parse(JSON5.stringify(value)); | ||
} | ||
pointable(json).at(options.pointer).set(value); | ||
console.log('Updated file in project', project.name.toString()); | ||
await file.write(json); | ||
}) | ||
.addCommand('create', 'Create a new Project', projectCreateSchema, async (workspace, _, options) => { | ||
@@ -97,0 +119,0 @@ await workspace.createProject(options); |
import { Project } from '@bscotch/project'; | ||
import { JsonSchema } from '@bscotch/validation'; | ||
import { Static, TObject } from '@sinclair/typebox'; | ||
import { Command } from 'commander'; | ||
@@ -13,2 +14,3 @@ import { Promisable } from 'type-fest'; | ||
}); | ||
addCommand<Schema extends TObject<any>, Options extends Static<Schema>>(name: string, description: string, schema: Schema, action: (workspace: Workspace, project: Project | undefined, options: Options) => Promisable<any>): TrebCli; | ||
addCommand<Options extends Record<string, any>>(name: string, description: string, schema: JsonSchema<Options>, action: (workspace: Workspace, project: Project | undefined, options: Options) => Promisable<any>): TrebCli; | ||
@@ -15,0 +17,0 @@ parse(): void; |
import { useTracer } from '@bscotch/utility'; | ||
import { JsonSchemaNode, prettifyErrorTracing, validate, } from '@bscotch/validation'; | ||
import { JsonSchemaNode, validate } from '@bscotch/validation'; | ||
import { ok } from 'assert'; | ||
@@ -54,2 +54,5 @@ import { Command } from 'commander'; | ||
} | ||
if (options.includeRoot) { | ||
projects.push(workspace); | ||
} | ||
for (const project of projects) { | ||
@@ -73,12 +76,13 @@ const cwd = process.cwd(); | ||
} | ||
prettifyErrorTracing({ | ||
replaceFilePaths: process.env.BSCOTCH_REPO === '@bscotch/tech' | ||
? [ | ||
{ | ||
pattern: /^(.*[\\/])node_modules[\\/]@bscotch([\\/].+)$/, | ||
replacer: '$1projects$2', | ||
}, | ||
] | ||
: undefined, | ||
}); | ||
// prettifyErrorTracing({ | ||
// replaceFilePaths: | ||
// process.env.BSCOTCH_REPO === '@bscotch/tech' | ||
// ? [ | ||
// { | ||
// pattern: /^(.*[\\/])node_modules[\\/]@bscotch([\\/].+)$/, | ||
// replacer: '$1projects$2', | ||
// }, | ||
// ] | ||
// : undefined, | ||
// }); | ||
//# sourceMappingURL=treb.lib.js.map |
import { ProjectExtractOptions, ProjectFixOptions, ProjectMoveFilesOptions, ProjectMoveOptions, ProjectRenameOptions, ProjectTestOptions } from '@bscotch/project'; | ||
import { JsonSchema } from '@bscotch/validation'; | ||
import { TProperties } from '@sinclair/typebox'; | ||
export interface TrebuchetProjectTargetOptions { | ||
targetProject?: string; | ||
allProjects?: boolean; | ||
includeRoot?: boolean; | ||
} | ||
export declare function projectOptions<S extends TProperties>(schema: S): import("@sinclair/typebox").TObject<S & { | ||
targetProject: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString<string>>; | ||
allProjects: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TBoolean>; | ||
includeRoot: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TBoolean>; | ||
}>; | ||
export declare const updateJsonOptionsSchema: { | ||
file: import("@sinclair/typebox").TString<string>; | ||
pointer: import("@sinclair/typebox").TString<string>; | ||
value: import("@sinclair/typebox").TString<string>; | ||
}; | ||
export interface TrebuchetExtractOptions extends ProjectExtractOptions, TrebuchetProjectTargetOptions { | ||
@@ -8,0 +20,0 @@ } |
import { projectExtractSchema, projectFixSchema, projectMoveFilesSchema, projectMoveSchema, projectRenameSchema, projectTestingSchema, } from '@bscotch/project'; | ||
import { merge } from '@bscotch/utility'; | ||
import { Type as t } from '@sinclair/typebox'; | ||
const projectTargetOptionsSchema = { | ||
targetProject: t.Optional(t.String()), | ||
allProjects: t.Optional(t.Boolean()), | ||
includeRoot: t.Optional(t.Boolean()), | ||
}; | ||
export function projectOptions(schema) { | ||
return t.Object({ | ||
...schema, | ||
...projectTargetOptionsSchema, | ||
}); | ||
} | ||
export const updateJsonOptionsSchema = { | ||
file: t.String({ | ||
description: "The name of the file to update, relative to the project's root.", | ||
pattern: '.*\\.(json|yaml|yml)$', | ||
}), | ||
pointer: t.String({ | ||
description: 'A JSON pointer to the field to update. Note that in some shells (e.g. bash) a value starting with a `/` is interpreted as a slash -- just add another slash to escape it. E.g. "/your/pointer" becomes "/your/pointer".', | ||
}), | ||
value: t.String({ | ||
description: 'The value to set the field to. Parsed using JSON5.', | ||
}), | ||
}; | ||
export const trebuchetProjectOption = { | ||
@@ -13,5 +37,10 @@ type: 'object', | ||
type: 'boolean', | ||
description: 'Run the command on all projects.', | ||
description: 'Run the command on all projects. Not supported by all commands.', | ||
default: false, | ||
}, | ||
includeRoot: { | ||
type: 'boolean', | ||
description: 'Run the command on the root project. Not supported by all commands.', | ||
default: false, | ||
}, | ||
}, | ||
@@ -18,0 +47,0 @@ }; |
@@ -1,3 +0,3 @@ | ||
import { Pathy, PathyOrString } from '@bscotch/pathy'; | ||
import { GitLogStatic } from './gitLog.static.js'; | ||
import { PathyOrString } from '@bscotch/pathy'; | ||
import { GitLogAffectedFile, GitLogStatic } from './gitLog.static.js'; | ||
import { GitLogChangeDescription, GitLogData, GitVersionTagParsed } from './gitLog.types.js'; | ||
@@ -11,3 +11,3 @@ export * from './gitLog.static.js'; | ||
readonly date: Date; | ||
readonly files: Pathy[]; | ||
readonly files: GitLogAffectedFile[]; | ||
readonly changes: GitLogChangeDescription[]; | ||
@@ -14,0 +14,0 @@ constructor(raw: GitLogData, repoRoot?: PathyOrString); |
import { Pathy, PathyOrString } from '@bscotch/pathy'; | ||
import { ArrayOrSingleton, PartialBy } from '@bscotch/utility'; | ||
import type { DiffResult } from 'simple-git'; | ||
import type { DiffResult, DiffResultBinaryFile, DiffResultTextFile } from 'simple-git'; | ||
import { GitLog } from './gitLog.js'; | ||
@@ -14,2 +14,14 @@ import { GitLogBump, GitLogChangeDescription, GitVersionTagParsed } from './gitLog.types.js'; | ||
/** | ||
* When trying to understand whether or not a given file has | ||
* changed, we often filter git logs by the *current* file path. | ||
* But since the file path may have changed, we also need to keep | ||
* track of renames so that we can also discover changes to the | ||
* file when it had a different name. | ||
*/ | ||
export declare class GitLogAffectedFile extends Pathy { | ||
readonly diff: DiffResultTextFile | DiffResultBinaryFile; | ||
readonly renamedFrom?: Pathy; | ||
constructor(diff: DiffResultTextFile | DiffResultBinaryFile, cwd: PathyOrString); | ||
} | ||
/** | ||
* Collection of static methods used by the | ||
@@ -43,3 +55,3 @@ * {@link GitLog} class. | ||
static tagToParsedProjectVersion(tag: string): GitVersionTagParsed | undefined; | ||
static affectedFiles(diff?: DiffResult, root?: PathyOrString): Pathy[]; | ||
static affectedFiles(diff?: DiffResult, root?: PathyOrString): GitLogAffectedFile[]; | ||
/** | ||
@@ -46,0 +58,0 @@ * Convert a parsed git log message, |
@@ -86,2 +86,30 @@ import { Pathy } from '@bscotch/pathy'; | ||
/** | ||
* When trying to understand whether or not a given file has | ||
* changed, we often filter git logs by the *current* file path. | ||
* But since the file path may have changed, we also need to keep | ||
* track of renames so that we can also discover changes to the | ||
* file when it had a different name. | ||
*/ | ||
export class GitLogAffectedFile extends Pathy { | ||
diff; | ||
renamedFrom; | ||
constructor(diff, cwd) { | ||
// The path could include a RENAME action, which we want to | ||
// keep track of. | ||
let logPath = diff.file; | ||
const renamePattern = /\{(?<oldName>.*?) => (?<newName>[^}]+)\}/; | ||
const renameMatch = logPath.match(renamePattern); | ||
let oldName; | ||
if (renameMatch) { | ||
oldName = logPath.replace(renamePattern, '$1'); | ||
logPath = logPath.replace(renamePattern, '$2'); | ||
} | ||
super(logPath, cwd); | ||
this.diff = diff; | ||
if (oldName) { | ||
this.renamedFrom = new Pathy(oldName, cwd); | ||
} | ||
} | ||
} | ||
/** | ||
* Collection of static methods used by the | ||
@@ -137,4 +165,4 @@ * {@link GitLog} class. | ||
return (diff?.files.map((file) => | ||
// Moved files show up with as `./path/{from => to}/etc.ext` | ||
new Pathy(file.file.replace(/\{.*?=> ([^}]+)\}/, '$1'), root)) || []); | ||
// Moved files show up as `./path/{from => to}/etc.ext` | ||
new GitLogAffectedFile(file, root)) || []); | ||
} | ||
@@ -141,0 +169,0 @@ /** |
@@ -35,2 +35,6 @@ import { DependencyVersion, PackageName } from '@bscotch/config'; | ||
}; | ||
interface ProjectInfo { | ||
name?: string | PackageName; | ||
version?: string | DependencyVersion; | ||
} | ||
/** | ||
@@ -46,6 +50,6 @@ * A helper class for managing a git repository, | ||
constructor(dir: Pathy); | ||
addProjectVersionTag(project: { | ||
name: string | PackageName | undefined; | ||
version: string | DependencyVersion | undefined; | ||
}): Promise<void>; | ||
computeProjectVersionTag(project: ProjectInfo): string; | ||
addProjectVersionTag(project: ProjectInfo): Promise<void>; | ||
latestProjectVersion(projectName: string | PackageName): Promise<string | undefined>; | ||
listProjectVersionTags(projectName: string | PackageName): Promise<string[]>; | ||
fetch(): Promise<void>; | ||
@@ -68,2 +72,3 @@ pull(): Promise<void>; | ||
} | ||
export {}; | ||
//# sourceMappingURL=repo.d.ts.map |
@@ -7,2 +7,3 @@ import { __decorate, __metadata } from "tslib"; | ||
import { ok } from 'assert'; | ||
import semver from 'semver'; | ||
import { default as simpleGit } from 'simple-git'; | ||
@@ -23,3 +24,3 @@ import { GitLog } from './gitLog.js'; | ||
} | ||
async addProjectVersionTag(project) { | ||
computeProjectVersionTag(project) { | ||
ok(project.name, 'Project name is required'); | ||
@@ -31,9 +32,22 @@ ok(project.version, 'Project version is required'); | ||
ok(DependencyVersion.isSemver(projectVersion), `Version ${projectVersion} is not a valid semver`); | ||
const tag = new PackageName({ | ||
return new PackageName({ | ||
name: projectName, | ||
version: projectVersion, | ||
}).toString({ includeVersion: true }); | ||
} | ||
async addProjectVersionTag(project) { | ||
const tag = this.computeProjectVersionTag(project); | ||
// Add the tag | ||
await this.git.addAnnotatedTag(tag, `Version ${projectVersion} of ${projectName}`); | ||
await this.git.addAnnotatedTag(tag, `Bumped the version`); | ||
} | ||
async latestProjectVersion(projectName) { | ||
const tags = await this.listProjectVersionTags(projectName); | ||
return tags.at(-1); | ||
} | ||
async listProjectVersionTags(projectName) { | ||
const pattern = `${projectName}@*`; | ||
const tags = (await this.git.tags(['--list', pattern])).all.map((t) => t.replace(/^.*@([^@]+)$/, '$1')); | ||
tags.sort(semver.compare); | ||
return tags; | ||
} | ||
async fetch() { | ||
@@ -72,14 +86,31 @@ await this.git.fetch(['--all', '--tags', '--prune']); | ||
}; | ||
return (await this.git.log(logOptions)).all.map((raw) => new GitLog(raw, this.dir)); | ||
const logs = (await this.git.log(logOptions)).all.map((raw) => new GitLog(raw, this.dir)); | ||
return logs; | ||
} | ||
async logs(options) { | ||
const onlyIfFoldersImpacted = options?.onlyIfFoldersImpacted?.map((f) => new Pathy(f, this.dir)); | ||
// const workerLogs = await gitLogPool.exec('gitLog', [this.dir.absolute]); | ||
// timer.mark('worker logged'); | ||
const logs = await this.allLogs(); | ||
// Track file renames so that we can map old | ||
// names onto the *current* folders. | ||
const renamedFileMap = {}; | ||
const keepLogs = []; | ||
for (const log of logs) { | ||
const includesImpactedFolders = !onlyIfFoldersImpacted || | ||
log.files.some((filePath) => onlyIfFoldersImpacted.some((folder) => folder.isParentOf(filePath))); | ||
if (includesImpactedFolders) { | ||
let foldersAreImpacted = false; | ||
if (onlyIfFoldersImpacted) { | ||
for (const affectedFile of log.files) { | ||
// Map the old name to the current name, if needed | ||
const filePath = renamedFileMap[affectedFile.relative] || affectedFile; | ||
// If this was a rename operation, store the rename | ||
if (affectedFile.renamedFrom) { | ||
renamedFileMap[affectedFile.renamedFrom.relative] = filePath; | ||
} | ||
// If the file is in one of the folders we care about, keep it. Only one match is required, since | ||
// we are operating at the entire-commit level | ||
if (onlyIfFoldersImpacted.some((folder) => folder.isParentOf(filePath))) { | ||
foldersAreImpacted = true; | ||
break; | ||
} | ||
} | ||
} | ||
if (!onlyIfFoldersImpacted || foldersAreImpacted) { | ||
keepLogs.push(log); | ||
@@ -86,0 +117,0 @@ } |
@@ -1,7 +0,7 @@ | ||
import { PackageJson, PackageNameConstructable, PackageNameEqualityCheckOptions, PackageNameEqualityOperand, VscodeWorkspace, VscodeWorkspaceFolder } from '@bscotch/config'; | ||
import { PackageNameConstructable, PackageNameEqualityCheckOptions, PackageNameEqualityOperand, VscodeWorkspace, VscodeWorkspaceFolder } from '@bscotch/config'; | ||
import { Pathy, PathyOrString } from '@bscotch/pathy'; | ||
import { Project, ProjectExtractOptions } from '@bscotch/project'; | ||
import { MemoizedClass, TracedClass } from '@bscotch/utility'; | ||
import { DepGraph } from 'dependency-graph'; | ||
import { Repo } from './repo.js'; | ||
import { ProjectGraph, WorkspaceDependencyGraphOptions } from './workspace.depGraph.js'; | ||
import type { WorkspaceConfig, WorkspaceCreateProjectOptions, WorkspaceFindOptions, WorkspaceImportProject, WorkspaceOptions, WorkspacePublishOptions } from './workspace.types.js'; | ||
@@ -18,10 +18,8 @@ export * from './workspace.schemas.js'; | ||
*/ | ||
export declare class Workspace { | ||
readonly options?: WorkspaceOptions | undefined; | ||
readonly dir: Pathy; | ||
readonly packageJson: PackageJson<{ | ||
trebuchet?: WorkspaceConfig; | ||
}>; | ||
readonly repo: Repo; | ||
constructor(options?: WorkspaceOptions | undefined); | ||
export declare class Workspace extends Project<{ | ||
trebuchet?: WorkspaceConfig; | ||
}> { | ||
private _repo?; | ||
constructor(options?: WorkspaceOptions); | ||
get repo(): Repo; | ||
/** | ||
@@ -34,10 +32,22 @@ * Returns the scope listed in the `package.json>trebuchet.npmScope` field, normalized to | ||
* Determine bump level for all changed | ||
* packages, bump them, and commit, and publish. | ||
* packages, bump them, update changelogs, and commit. | ||
* | ||
* Likely followed by a {@link Workspace.publishProjects} call. | ||
*/ | ||
versionProjects(options: WorkspacePublishOptions): Promise<Project[]>; | ||
/** | ||
* Publish each project whose local `package.json` version | ||
* is greater than any found in the tags. | ||
*/ | ||
publishProjects(options: WorkspacePublishOptions): Promise<void>; | ||
/** | ||
* This this workspace have a turbo.json file (and is | ||
* thus assumed to be using turborepo)? | ||
*/ | ||
hasTurbo(): Promise<Pathy<unknown> | undefined>; | ||
/** | ||
* Compute the dependency graph for all of the | ||
* projects in this workspace. | ||
*/ | ||
dependencyGraph(): Promise<DepGraph<Project>>; | ||
dependencyGraph(options?: WorkspaceDependencyGraphOptions): Promise<ProjectGraph>; | ||
/** | ||
@@ -80,3 +90,3 @@ * Show which projects are using which external | ||
moveProject(project: Project, where: PathyOrString): Promise<void>; | ||
extractProject(project: Project, options: ProjectExtractOptions): Promise<void>; | ||
extractNewProject(project: Project, options: ProjectExtractOptions): Promise<void>; | ||
/** | ||
@@ -83,0 +93,0 @@ * Rename a project, cascading the change to all |
import { Project } from '@bscotch/project'; | ||
import { DepGraph } from 'dependency-graph'; | ||
import type { Workspace } from './workspace.js'; | ||
export declare function computeProjectDependencyGraph(workspace: Workspace): Promise<DepGraph<Project>>; | ||
export interface WorkspaceDependencyGraphOptions { | ||
excludeDevDependencies?: boolean; | ||
} | ||
export declare class ProjectGraph { | ||
protected graph: DepGraph<Project>; | ||
constructor(projects: Project[], options?: WorkspaceDependencyGraphOptions); | ||
dependentsOf(project: Project): Project[]; | ||
/** | ||
* Get a shallow copy of the list of all projects in the graph. | ||
* | ||
* Shorthand for `[...this]`. | ||
*/ | ||
list(): Project[]; | ||
/** | ||
* Iterate over the projects in order of the dependency graph, | ||
* with leaves first. | ||
*/ | ||
[Symbol.iterator](): Iterator<Project>; | ||
getProject(name: string): Project; | ||
/** | ||
* Add a project to the graph. | ||
*/ | ||
protected addProject(project: Project): void; | ||
protected addProjectDependency(from: Project, to: Project): void; | ||
} | ||
//# sourceMappingURL=workspace.depGraph.d.ts.map |
import { assertUserClaim } from '@bscotch/validation'; | ||
import { DepGraph } from 'dependency-graph'; | ||
export async function computeProjectDependencyGraph(workspace) { | ||
const projects = await workspace.listProjects(); | ||
const graph = new DepGraph(); | ||
for (const project of projects) { | ||
graph.addNode(project.name.toString(), project); | ||
// Check the deps for other projects | ||
for (const otherProject of projects) { | ||
if (project === otherProject) { | ||
continue; | ||
export class ProjectGraph { | ||
graph; | ||
constructor(projects, options = {}) { | ||
this.graph = new DepGraph(); | ||
for (const project of projects) { | ||
this.addProject(project); | ||
// Check the deps for other projects | ||
for (const otherProject of projects) { | ||
if (project === otherProject) { | ||
continue; | ||
} | ||
const dep = project.packageJson.findDependency(otherProject.name); | ||
// Only changes to non-dev deps | ||
if (dep && | ||
(!options?.excludeDevDependencies || dep.type === 'dependencies')) { | ||
this.addProjectDependency(project, otherProject); | ||
} | ||
} | ||
const dep = project.packageJson.findDependency(otherProject.name); | ||
if (dep) { | ||
assertUserClaim(project.packageJson.canDependOn(otherProject.packageJson), `${project.name} cannot depend on ${otherProject.name} due to publishing access differences.`); | ||
graph.addNode(otherProject.name.toString(), otherProject); | ||
graph.addDependency(project.name.toString(), otherProject.name.toString()); | ||
} | ||
} | ||
} | ||
return graph; | ||
dependentsOf(project) { | ||
return this.graph | ||
.dependentsOf(project.name.toString()) | ||
.map((name) => this.getProject(name)); | ||
} | ||
/** | ||
* Get a shallow copy of the list of all projects in the graph. | ||
* | ||
* Shorthand for `[...this]`. | ||
*/ | ||
list() { | ||
return [...this]; | ||
} | ||
/** | ||
* Iterate over the projects in order of the dependency graph, | ||
* with leaves first. | ||
*/ | ||
*[Symbol.iterator]() { | ||
for (const project of this.graph.overallOrder()) { | ||
yield this.graph.getNodeData(project); | ||
} | ||
} | ||
getProject(name) { | ||
return this.graph.getNodeData(name); | ||
} | ||
/** | ||
* Add a project to the graph. | ||
*/ | ||
addProject(project) { | ||
this.graph.addNode(project.name.toString(), project); | ||
} | ||
addProjectDependency(from, to) { | ||
// Ensure the dep has a node | ||
assertUserClaim(from.packageJson.canDependOn(to.packageJson), `${from.name} cannot depend on ${to.name} due to publishing access differences.`); | ||
this.addProject(to); | ||
this.graph.addDependency(from.name.toString(), to.name.toString()); | ||
} | ||
} | ||
//# sourceMappingURL=workspace.depGraph.js.map |
@@ -10,3 +10,3 @@ var Workspace_1; | ||
import { Repo } from './repo.js'; | ||
import { computeProjectDependencyGraph } from './workspace.depGraph.js'; | ||
import { ProjectGraph, } from './workspace.depGraph.js'; | ||
import { extractProject } from './workspace.extract.js'; | ||
@@ -16,3 +16,3 @@ import { importProjectIntoWorkspace } from './workspace.import.js'; | ||
import { moveWorkspaceProject } from './workspace.move.js'; | ||
import { publishProjects } from './workspace.publish.js'; | ||
import { publishProjects, versionProjects } from './workspace.publish.js'; | ||
import { addWorkspaceFolderToVscodeConfig, getWorkspaceVscodeConfig, } from './workspace.vscode.js'; | ||
@@ -28,15 +28,11 @@ export * from './workspace.schemas.js'; | ||
*/ | ||
let Workspace = Workspace_1 = class Workspace { | ||
options; | ||
dir; | ||
packageJson; | ||
repo; | ||
let Workspace = Workspace_1 = class Workspace extends Project { | ||
_repo; | ||
constructor(options) { | ||
this.options = options; | ||
this.dir = Pathy.asInstance(options?.dir); | ||
this.packageJson = | ||
options?.packageJson || new PackageJson({ dir: this.dir }); | ||
this.repo = new Repo(this.dir); | ||
return this; | ||
super(options); | ||
} | ||
get repo() { | ||
this._repo ||= new Repo(this.dir); | ||
return this._repo; | ||
} | ||
/** | ||
@@ -55,4 +51,13 @@ * Returns the scope listed in the `package.json>trebuchet.npmScope` field, normalized to | ||
* Determine bump level for all changed | ||
* packages, bump them, and commit, and publish. | ||
* packages, bump them, update changelogs, and commit. | ||
* | ||
* Likely followed by a {@link Workspace.publishProjects} call. | ||
*/ | ||
async versionProjects(options) { | ||
return await versionProjects(this, options); | ||
} | ||
/** | ||
* Publish each project whose local `package.json` version | ||
* is greater than any found in the tags. | ||
*/ | ||
async publishProjects(options) { | ||
@@ -62,7 +67,14 @@ return await publishProjects.bind(this)(options); | ||
/** | ||
* This this workspace have a turbo.json file (and is | ||
* thus assumed to be using turborepo)? | ||
*/ | ||
async hasTurbo() { | ||
return await this.dir.findChild('turbo.json'); | ||
} | ||
/** | ||
* Compute the dependency graph for all of the | ||
* projects in this workspace. | ||
*/ | ||
async dependencyGraph() { | ||
return await computeProjectDependencyGraph(this); | ||
async dependencyGraph(options) { | ||
return new ProjectGraph(await this.listProjects(), options); | ||
} | ||
@@ -153,3 +165,3 @@ /** | ||
} | ||
async extractProject(project, options) { | ||
async extractNewProject(project, options) { | ||
return await extractProject.bind(this)(project, options); | ||
@@ -156,0 +168,0 @@ } |
@@ -36,4 +36,3 @@ import { Pathy } from '@bscotch/pathy'; | ||
await Promise.all(roots.map((r) => r.listChildrenRecursively({ | ||
async onInclude(path) { | ||
const pkg = await path.read(); | ||
onInclude(path) { | ||
projects.push(new Project({ | ||
@@ -40,0 +39,0 @@ dir: path.up(), |
@@ -6,3 +6,3 @@ import { Project } from '@bscotch/project'; | ||
* Determine bump level for all changed | ||
* packages, bump them, and commit, and publish. | ||
* packages, bump them, and commit | ||
* | ||
@@ -18,3 +18,20 @@ * Assumes that: | ||
export declare function publishProjects(this: Workspace, options: WorkspacePublishOptions): Promise<void>; | ||
/** | ||
* Determine bump level for all changed | ||
* packages, bump them, update changelogs, and commit. | ||
* | ||
* @remarks | ||
* Version tags are not added here. They are added during the | ||
* publishing step. | ||
* | ||
* Assumes that: | ||
* - Version tags are the "truth" for which commits correspond to versioning+publishing events. | ||
* - Tags are in format `@bscotch/utility@4.0.0` | ||
* - Current `package.json` `version` field is the latest version. | ||
* - The changed-files of a commit can be used to infer whether the commit applied to a given project | ||
* - The bump level can be inferred using the conventional-commit strategy. | ||
* - The changelog should be *added to* instead of *replaced*, assuming that the changelog has already been written up to the last published version. | ||
*/ | ||
export declare function versionProjects(workspace: Workspace, options: WorkspacePublishOptions): Promise<Project[]>; | ||
export declare function inferProjectBumpFromGitLogs(workspace: Workspace, project: Project): Promise<import("./gitLog.types.js").GitLogBump | undefined>; | ||
//# sourceMappingURL=workspace.publish.d.ts.map |
import { deepEquals } from '@bscotch/utility'; | ||
import { ok } from 'assert'; | ||
import semver from 'semver'; | ||
import { GitLog } from './gitLog.js'; | ||
@@ -9,5 +11,12 @@ // import {default as latest} from 'latest-version'; | ||
} | ||
function normalizePublishOptions(options) { | ||
return { | ||
...options, | ||
noPush: anyAreTruthy([(options?.noCommit, options?.noPush)]), | ||
noTag: anyAreTruthy([(options?.noCommit, options?.noPush, options?.noTag)]), | ||
}; | ||
} | ||
/** | ||
* Determine bump level for all changed | ||
* packages, bump them, and commit, and publish. | ||
* packages, bump them, and commit | ||
* | ||
@@ -23,3 +32,3 @@ * Assumes that: | ||
export async function publishProjects(options) { | ||
options.noPush = anyAreTruthy([(options?.noCommit, options?.noPush)]); | ||
options = normalizePublishOptions(options); | ||
// Make sure we're up to date! | ||
@@ -29,65 +38,127 @@ if (!options?.noPull) { | ||
} | ||
const versionedProjects = await versionProjects(this, options); | ||
await Promise.all(versionedProjects.map((p) => p.pruneCompiled())); | ||
await this.packageJson.test(versionedProjects.map((p) => `--filter=...${p.name}`)); | ||
await commitVersionedProjects(this, versionedProjects, options); | ||
for (const project of versionedProjects) { | ||
const bumpedProjects = (await listBumpedProjects(this)).filter((p) => p.packageJson.isPublishable); | ||
if (!bumpedProjects.length) { | ||
console.warn('No bumped projects found.'); | ||
return; | ||
} | ||
const filterMap = ({ dependents, dependencies, } = {}) => (project) => `--filter=${dependencies ? '...' : ''}${project.name}${dependents ? '...' : ''}`; | ||
// Make sure all deps are installed etc! | ||
await this.packageJson.execute([ | ||
'install', | ||
...bumpedProjects.map(filterMap({ dependencies: true, dependents: true })), | ||
]); | ||
// Make sure we're freshly built to ensure that built files | ||
// match the source (and thus that passing tests are accurate). | ||
const canRunTasks = (await this.hasTurbo()) && this.packageJson.packageManager === 'pnpm'; | ||
ok(canRunTasks || (options?.noRebuild && options?.noTest), 'Currently only pnpm + turborepo are supported for cleaning and testing prior to publishing. Re-run with --noClean and --noTest to skip those steps.'); | ||
if (canRunTasks) { | ||
const tasks = []; | ||
if (!options?.noRebuild) { | ||
tasks.push('build'); | ||
} | ||
if (!options?.noTest) { | ||
tasks.push('test'); | ||
} | ||
if (tasks.length) { | ||
await this.packageJson.execute([ | ||
'turbo', | ||
'run', | ||
...tasks, | ||
...bumpedProjects.map(filterMap({ dependencies: true, dependents: true })), | ||
]); | ||
} | ||
} | ||
// For each project, in turn, + tag + publish (abort if any fail, which will prevent publishing of dependents and also prevent wonky tag situations). | ||
for (const project of bumpedProjects) { | ||
if (!options.noTag) { | ||
await this.repo.addProjectVersionTag(project); | ||
} | ||
else { | ||
console.warn(`noTag is true -- would have tagged ${project.name} with ${this.repo.computeProjectVersionTag(project)}`); | ||
} | ||
await project.packageJson.publish({ dryRun: options.noPush }); | ||
if (!options.noPush) { | ||
await this.repo.push(); | ||
} | ||
} | ||
} | ||
async function commitVersionedProjects(workspace, bumpedProjects, options = {}) { | ||
// Add *all* package.json, not just bumped | ||
// ones, since some are not publishable but | ||
// will have had their deps updated. | ||
const projects = await workspace.listProjects(); | ||
await workspace.repo.add(projects.map((p) => p.packageJson.path)); | ||
if (!options?.noCommit) { | ||
await workspace.repo.commit(`publish: ${bumpedProjects | ||
.map((p) => `${p.name}@${p.version}`) | ||
.join(', ')}`); | ||
} | ||
// Only add tags for bumped projects | ||
if (!options?.noTag && !options?.noCommit) { | ||
// Add tags! | ||
for (const project of bumpedProjects) { | ||
await workspace.repo.addProjectVersionTag(project); | ||
/** | ||
* List the projects in the workspace that have had a version | ||
* bump (compared to the latest git tags). | ||
* | ||
* Throws if any bumped project's dependents are not also bumped. | ||
*/ | ||
async function listBumpedProjects(workspace) { | ||
const graph = await workspace.dependencyGraph({ | ||
excludeDevDependencies: true, | ||
}); | ||
const bumped = []; | ||
for (const project of graph) { | ||
// If we've already added it, move along! | ||
if (bumped.find((p) => p.equals(project))) { | ||
continue; | ||
} | ||
if (!(await projectIsBumped(workspace, project))) { | ||
continue; | ||
} | ||
bumped.push(project); | ||
// Make sure that dependents have also been versioned. | ||
for (const dep of graph.dependentsOf(project)) { | ||
ok(await projectIsBumped(workspace, dep), `Dependent ${dep.name} of bumped project ${project.name} must also be bumped!`); | ||
bumped.push(dep); | ||
} | ||
} | ||
if (!options?.noPush) { | ||
await workspace.repo.push(); | ||
return bumped; | ||
} | ||
async function projectIsBumped(workspace, project) { | ||
const publishedVersion = await workspace.repo.latestProjectVersion(project.name); | ||
const currentVersion = project.packageJson.version; | ||
ok(!publishedVersion || | ||
semver.gte(currentVersion.toString(), publishedVersion), `Project ${project.name} has version ${currentVersion} in its package.json, which is less than the latest published ${publishedVersion}`); | ||
if (publishedVersion && | ||
semver.eq(currentVersion.toString(), publishedVersion)) { | ||
return false; | ||
} | ||
return true; | ||
} | ||
/** | ||
* Version all all projects in the workspace, based | ||
* on inferred or provided options. | ||
* Determine bump level for all changed | ||
* packages, bump them, update changelogs, and commit. | ||
* | ||
* Returns a list of all projects that were versioned. | ||
* @remarks | ||
* Version tags are not added here. They are added during the | ||
* publishing step. | ||
* | ||
* Assumes that: | ||
* - Version tags are the "truth" for which commits correspond to versioning+publishing events. | ||
* - Tags are in format `@bscotch/utility@4.0.0` | ||
* - Current `package.json` `version` field is the latest version. | ||
* - The changed-files of a commit can be used to infer whether the commit applied to a given project | ||
* - The bump level can be inferred using the conventional-commit strategy. | ||
* - The changelog should be *added to* instead of *replaced*, assuming that the changelog has already been written up to the last published version. | ||
*/ | ||
async function versionProjects(workspace, options) { | ||
const projects = await workspace.listProjects(); | ||
const projectDepGraph = await workspace.dependencyGraph(); | ||
export async function versionProjects(workspace, options) { | ||
options = normalizePublishOptions(options); | ||
// Make sure we're up to date (especially for tags) | ||
if (!options?.noPull) { | ||
await workspace.repo.pull(); | ||
} | ||
const bumpedProjects = []; | ||
// // Get the current published versions of | ||
// // everything to compare against. | ||
// const publishedVersions = await Promise.all(projects.map(async p => { | ||
// const v = await latest(p.name.toString()); | ||
// return { name: p.name.toString(), version: v }; | ||
// })); | ||
// Work through the dependency graph and bump | ||
// versions, plus update dependency versions. | ||
// By doing this in order overall topological order | ||
// we can do it all in one pass. | ||
for (const projectName of projectDepGraph.overallOrder()) { | ||
// Update this project, if necessary | ||
const project = projectDepGraph.getNodeData(projectName); | ||
if (!project.packageJson.isPublishable) { | ||
// No need to bump, or do anything else really. | ||
continue; | ||
} | ||
const projectDepGraph = await workspace.dependencyGraph({ | ||
excludeDevDependencies: true, | ||
}); | ||
// Work through the dependency graph: | ||
// 1. bump versions if the changelog warrants it | ||
// 2. bump dependents of bumped projects | ||
// 3. TODO: Add dependency-bump to changelog of dependents | ||
// 4. TODO: Update the changelog of the bumped project | ||
for (const project of projectDepGraph) { | ||
let bump = options?.bump || (await inferProjectBumpFromGitLogs(workspace, project)); | ||
workspace.trace(`Inferred bump ${projectName}`, bump); | ||
workspace.trace(`Inferred bump ${project.name}`, bump); | ||
// Ensure all deps, including local deps, | ||
// are up to date. | ||
const pkgInitial = project.packageJson.toJSON(); | ||
await project.updateDependencyListings({ localPackages: projects }); | ||
await project.updateDependencyListings({ | ||
localPackages: projectDepGraph.list(), | ||
}); | ||
// Detect any changes that would require a bump, if we aren't already bumping | ||
@@ -101,3 +172,3 @@ if (!bump) { | ||
bump = isChanged || localDepHasChanged ? 'patch' : undefined; | ||
workspace.trace(`Checked deps changes for bump ${projectName}`, bump); | ||
workspace.trace(`Checked deps changes for bump ${project.name}`, bump); | ||
} | ||
@@ -107,10 +178,14 @@ // Update the version, if necessary | ||
await project.packageJson.bumpVersion(bump); | ||
// Update placeholders in CHANGELOG files etc | ||
const changelog = await project.dir.findChild('CHANGELOG.md'); | ||
if (changelog) { | ||
await changelog.write((await changelog.read()).replace(/\{\{\s*next\s*\}\}/g, project.packageJson.version.toString())); | ||
} | ||
bumpedProjects.push(project); | ||
} | ||
} | ||
if (!options.noCommit) { | ||
await workspace.repo.add(bumpedProjects.map((p) => p.packageJson.path)); | ||
await workspace.repo.commit(`bumped: ${bumpedProjects | ||
.map((p) => `${p.name}@${p.version}`) | ||
.join(', ')}`); | ||
if (!options.noPush) { | ||
await workspace.repo.push(); | ||
} | ||
} | ||
return bumpedProjects; | ||
@@ -117,0 +192,0 @@ } |
@@ -52,6 +52,2 @@ import { merge } from '@bscotch/utility'; | ||
}, | ||
noTag: { | ||
type: 'boolean', | ||
description: 'If true, then no tag will be added to the resulting commit. This is forced to be `true` if `noCommit` is true.', | ||
}, | ||
noPush: { | ||
@@ -71,2 +67,12 @@ type: 'boolean', | ||
}, | ||
noRebuild: { | ||
type: 'boolean', | ||
}, | ||
noTag: { | ||
type: 'boolean', | ||
description: 'If true, then no tag will be added to the resulting commit. This is forced to be `true` if `noCommit` is true.', | ||
}, | ||
noTest: { | ||
type: 'boolean', | ||
}, | ||
}, | ||
@@ -73,0 +79,0 @@ }); |
@@ -121,8 +121,2 @@ import type { PackageJsonData, PackageJsonFindOptions } from '@bscotch/config'; | ||
/** | ||
* If true, then no tag will be added to the resulting | ||
* commit. This is forced to be `true` if `noCommit` is | ||
* true. | ||
*/ | ||
noTag?: boolean; | ||
/** | ||
* If true, then no push will be made to the remote. | ||
@@ -136,3 +130,19 @@ * This is forced to be `true` if `noCommit` is true, | ||
noPull?: boolean; | ||
/** | ||
* If true, then no tag will be added to the resulting | ||
* commit. This is forced to be `true` if `noCommit` is | ||
* true. | ||
*/ | ||
noTag?: boolean; | ||
/** | ||
* If true, then bumped projects will not be cleaned and | ||
* rebuilt prior to testing and publishing. | ||
*/ | ||
noRebuild?: boolean; | ||
/** | ||
* If true, then bumped projects will not be tested prior | ||
* to publishing. | ||
*/ | ||
noTest?: boolean; | ||
} | ||
//# sourceMappingURL=workspace.types.d.ts.map |
{ | ||
"name": "@bscotch/trebuchet", | ||
"version": "2.1.3", | ||
"version": "2.4.0", | ||
"description": "Tooling for minimizing the cognitive load for monorepo/workspace management, with a focus on automation, minimal configuration, and interoperability with other tools.", | ||
@@ -43,8 +43,8 @@ "keywords": [ | ||
"dependencies": { | ||
"@bscotch/config": "1.0.5", | ||
"@bscotch/pathy": "2.2.0", | ||
"@bscotch/project": "2.1.0", | ||
"@bscotch/utility": "6.2.0", | ||
"@bscotch/validation": "0.2.1", | ||
"chai": "^4.3.6", | ||
"@bscotch/config": "1.1.0", | ||
"@bscotch/pathy": "2.4.0", | ||
"@bscotch/project": "2.3.0", | ||
"@bscotch/utility": "6.4.0", | ||
"@bscotch/validation": "0.2.2", | ||
"@sinclair/typebox": "^0.24.43", | ||
"commander": "^9.3.0", | ||
@@ -54,13 +54,20 @@ "dependency-graph": "^0.11.0", | ||
"inquirer": "^9.0.0", | ||
"json5": "^2.2.1", | ||
"semver": "^7.3.7", | ||
"simple-git": "^3.10.0", | ||
"simple-git": "^3.14.0", | ||
"tslib": "^2.4.0", | ||
"type-fest": "^2.16.0", | ||
"type-fest": "^3.0.0", | ||
"yaml": "^2.1.1" | ||
}, | ||
"devDependencies": { | ||
"@local/helpers": "0.0.0", | ||
"@types/chai": "^4.3.3", | ||
"@types/fs-extra": "^9.0.13", | ||
"@types/inquirer": "^8.2.1", | ||
"@types/mocha": "^9.1.1", | ||
"@types/semver": "^7.3.10", | ||
"typescript": "4.9.0-dev.20220829" | ||
"chai": "^4.3.6", | ||
"mocha": "^10.0.0", | ||
"rimraf": "^3.0.2", | ||
"typescript": "4.9.1-beta" | ||
}, | ||
@@ -71,6 +78,8 @@ "publishConfig": { | ||
"scripts": { | ||
"build": "tsc --build", | ||
"test": "treb test", | ||
"build": "tsc-x --build", | ||
"clean": "rimraf build dist *.tsbuildinfo **/*.tsbuildinfo", | ||
"test": "mocha --config ../../config/.mocharc.cjs", | ||
"test:dev": "mocha --config ../../config/.mocharc.cjs --forbid-only=false --parallel=false --timeout=9999999999", | ||
"watch": "tsc --build --watch" | ||
} | ||
} |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
174452
2369
3
16
10
86
+ Added@sinclair/typebox@^0.24.43
+ Addedjson5@^2.2.1
+ Added@bscotch/config@1.1.0(transitive)
+ Added@bscotch/pathy@2.4.0(transitive)
+ Added@bscotch/project@2.3.0(transitive)
+ Added@bscotch/utility@6.4.0(transitive)
+ Added@bscotch/validation@0.2.2(transitive)
+ Added@sinclair/typebox@0.24.51(transitive)
+ Added@types/node@22.9.1(transitive)
+ Addedtype-fest@3.13.1(transitive)
- Removedchai@^4.3.6
- Removed@bscotch/config@1.0.5(transitive)
- Removed@bscotch/pathy@2.2.0(transitive)
- Removed@bscotch/project@2.1.0(transitive)
- Removed@bscotch/utility@6.2.0(transitive)
- Removed@bscotch/validation@0.2.1(transitive)
- Removed@types/node@22.9.3(transitive)
- Removedassertion-error@1.1.0(transitive)
- Removedchai@4.5.0(transitive)
- Removedcheck-error@1.0.3(transitive)
- Removeddeep-eql@4.1.4(transitive)
- Removedget-func-name@2.0.2(transitive)
- Removedloupe@2.3.7(transitive)
- Removedpathval@1.1.1(transitive)
- Removedtype-detect@4.1.0(transitive)
- Removedtype-fest@2.19.0(transitive)
Updated@bscotch/config@1.1.0
Updated@bscotch/pathy@2.4.0
Updated@bscotch/project@2.3.0
Updated@bscotch/utility@6.4.0
Updated@bscotch/validation@0.2.2
Updatedsimple-git@^3.14.0
Updatedtype-fest@^3.0.0