diff options
Diffstat (limited to 'packages/monorepo-scripts/src/utils')
3 files changed, 590 insertions, 1 deletions
diff --git a/packages/monorepo-scripts/src/utils/doc_generate_and_upload_utils.ts b/packages/monorepo-scripts/src/utils/doc_generate_and_upload_utils.ts new file mode 100644 index 000000000..5fee98b63 --- /dev/null +++ b/packages/monorepo-scripts/src/utils/doc_generate_and_upload_utils.ts @@ -0,0 +1,485 @@ +import { readFileSync, writeFileSync } from 'fs'; +import * as _ from 'lodash'; +import * as path from 'path'; +import { exec as execAsync } from 'promisify-child-process'; +import * as ts from 'typescript'; + +import { constants } from '../constants'; +import { docGenConfigs } from '../doc_gen_configs'; +import { ExportInfo, ExportNameToTypedocNames, ExportPathToExportedItems, PackageJSON } from '../types'; + +import { utils } from './utils'; + +export class DocGenerateAndUploadUtils { + private readonly _isStaging: boolean; + private readonly _shouldUploadDocs: boolean; + private readonly _packageName: string; + private readonly _omitExports: string[]; + private readonly _packagePath: string; + private readonly _exportPathToExportedItems: ExportPathToExportedItems; + private readonly _exportPathOrder: string[]; + private readonly _monoRepoPkgNameToPath: { [name: string]: string }; + private readonly _packageJson: PackageJSON; + /** + * Recursively iterate over the TypeDoc JSON object and find all type names + */ + private static _getAllTypeNames(node: any, typeNames: string[]): string[] { + if (!_.isObject(node)) { + return typeNames; + } + const typeKindStrings = ['Interface', 'Enumeration', 'Type alias']; + if (_.includes(typeKindStrings, node.kindString)) { + return [...typeNames, node.name]; + } + let updatedTypeNames = typeNames; + _.each(node, nodeValue => { + if (_.isArray(nodeValue)) { + _.each(nodeValue, aNode => { + updatedTypeNames = DocGenerateAndUploadUtils._getAllTypeNames(aNode, updatedTypeNames); + }); + } else if (_.isObject(nodeValue)) { + updatedTypeNames = DocGenerateAndUploadUtils._getAllTypeNames(nodeValue, updatedTypeNames); + } + }); + return updatedTypeNames; + } + /** + * Recursively iterate over the TypeDoc JSON object and find all reference names (i.e types, classNames, + * objectLiteral names, etc...) + */ + private static _getAllReferenceNames(propertyName: string, node: any, referenceNames: string[]): string[] { + if (!_.isObject(node)) { + return referenceNames; + } + + let updatedReferenceNames = referenceNames; + // Some nodes of type reference are for subtypes, which we don't want to return. + // We therefore filter them out. + const SUB_TYPE_PROPERTY_NAMES = ['inheritedFrom', 'overwrites', 'extendedTypes', 'implementationOf']; + if ( + !_.isUndefined(node.type) && + _.isString(node.type) && + node.type === 'reference' && + node.name !== 'Partial' && + node.name !== 'Promise' && + !_.includes(SUB_TYPE_PROPERTY_NAMES, propertyName) + ) { + updatedReferenceNames = _.uniq([...referenceNames, node.name]); + return updatedReferenceNames; + } + _.each(node, (nodeValue, innerPropertyName) => { + if (_.isArray(nodeValue)) { + _.each(nodeValue, aNode => { + updatedReferenceNames = DocGenerateAndUploadUtils._getAllReferenceNames( + innerPropertyName, + aNode, + updatedReferenceNames, + ); + }); + } else if (_.isObject(nodeValue)) { + updatedReferenceNames = updatedReferenceNames = DocGenerateAndUploadUtils._getAllReferenceNames( + innerPropertyName, + nodeValue, + updatedReferenceNames, + ); + } + }); + return _.uniq(updatedReferenceNames); + } + private static _getExportPathToExportedItems(filePath: string, omitExports?: string[]): ExportInfo { + const sourceFile = ts.createSourceFile( + 'indexFile', + readFileSync(filePath).toString(), + ts.ScriptTarget.ES2017, + /*setParentNodes */ true, + ); + const exportPathToExportedItems: ExportPathToExportedItems = {}; + const exportPathOrder: string[] = []; + const exportsToOmit = _.isUndefined(omitExports) ? [] : omitExports; + + processNode(sourceFile); + + function processNode(node: ts.Node): void { + switch (node.kind) { + case ts.SyntaxKind.ExportDeclaration: { + const exportClause = (node as any).exportClause; + const exportPath = exportClause.parent.moduleSpecifier.text; + _.each(exportClause.elements, element => { + const exportItem = element.name.escapedText; + if (!_.includes(exportsToOmit, exportItem)) { + exportPathToExportedItems[exportPath] = _.isUndefined(exportPathToExportedItems[exportPath]) + ? [exportItem] + : [...exportPathToExportedItems[exportPath], exportItem]; + } + }); + if (!_.isUndefined(exportPathToExportedItems[exportPath])) { + exportPathOrder.push(exportPath); + } + break; + } + + case ts.SyntaxKind.ExportKeyword: { + const foundNode: any = node; + let exportPath = './index'; + if (foundNode.parent && foundNode.parent.name) { + const exportItem = foundNode.parent.name.escapedText; + const isExportImportRequireStatement = !_.isUndefined( + _.get(foundNode, 'parent.moduleReference.expression.text'), + ); + if (isExportImportRequireStatement) { + exportPath = foundNode.parent.moduleReference.expression.text; + } + if (!_.includes(exportsToOmit, exportItem)) { + exportPathToExportedItems[exportPath] = _.isUndefined(exportPathToExportedItems[exportPath]) + ? [exportItem] + : [...exportPathToExportedItems[exportPath], exportItem]; + } + } + if ( + !_.includes(exportPathOrder, exportPath) && + !_.isUndefined(exportPathToExportedItems[exportPath]) + ) { + exportPathOrder.push(exportPath); + } + break; + } + default: + // noop + break; + } + + ts.forEachChild(node, processNode); + } + const exportInfo = { + exportPathToExportedItems, + exportPathOrder, + }; + return exportInfo; + } + constructor(packageName: string, isStaging: boolean, shouldUploadDocs: boolean) { + this._isStaging = isStaging; + this._packageName = packageName; + this._shouldUploadDocs = shouldUploadDocs; + this._packagePath = `${constants.monorepoRootPath}/packages/${packageName}`; + + this._monoRepoPkgNameToPath = {}; + const monorepoPackages = utils.getPackages(constants.monorepoRootPath); + _.each(monorepoPackages, p => (this._monoRepoPkgNameToPath[p.packageJson.name] = p.location)); + + const pkg = _.find(monorepoPackages, monorepoPackage => { + return _.includes(monorepoPackage.packageJson.name, packageName); + }); + if (_.isUndefined(pkg)) { + throw new Error(`Couldn't find a package.json for ${packageName}`); + } + this._packageJson = pkg.packageJson; + this._omitExports = _.get(this._packageJson, 'config.postpublish.docOmitExports', []); + + const indexPath = `${this._packagePath}/src/index.ts`; + const exportInfo = DocGenerateAndUploadUtils._getExportPathToExportedItems(indexPath, this._omitExports); + this._exportPathToExportedItems = exportInfo.exportPathToExportedItems; + this._exportPathOrder = exportInfo.exportPathOrder; + } + public async generateAndUploadDocsAsync(): Promise<void> { + // For each dep that is another one of our monorepo packages, we fetch it's index.ts + // and see which specific files we must pass to TypeDoc, in order to generate a Doc JSON + // the includes everything exported by the public interface. + const typeDocExtraFileIncludes: string[] = this._getTypeDocFileIncludesForPackage(); + + // In order to avoid TS errors, we need to pass TypeDoc the package's global.d.ts file + typeDocExtraFileIncludes.push(path.join(this._packagePath, 'src', 'globals.d.ts')); + + utils.log(`GENERATE_UPLOAD_DOCS: Generating Typedoc JSON for ${this._packageName}...`); + const jsonFilePath = path.join(this._packagePath, 'generated_docs', 'index.json'); + const projectFiles = typeDocExtraFileIncludes.join(' '); + const cwd = path.join(constants.monorepoRootPath, 'packages', this._packageName); + // HACK: For some reason calling `typedoc` command directly from here, even with `cwd` set to the + // packages root dir, does not work. It only works when called via a `package.json` script located + // in the package's root. + await execAsync(`JSON_FILE_PATH=${jsonFilePath} PROJECT_FILES="${projectFiles}" yarn docs:json`, { + cwd, + }); + + utils.log('GENERATE_UPLOAD_DOCS: Modifying Typedoc JSON to our custom format...'); + const typedocOutputString = readFileSync(jsonFilePath).toString(); + const typedocOutput = JSON.parse(typedocOutputString); + const standardizedTypedocOutput = this._standardizeTypedocOutputTopLevelChildNames(typedocOutput); + const modifiedTypedocOutput = this._pruneTypedocOutput(standardizedTypedocOutput); + + if (!_.includes(docGenConfigs.TYPES_ONLY_LIBRARIES, this._packageName)) { + const propertyName = ''; // Root has no property name + const referenceNames = DocGenerateAndUploadUtils._getAllReferenceNames( + propertyName, + modifiedTypedocOutput, + [], + ); + this._lookForUnusedExportedTypesThrowIfExists(referenceNames, modifiedTypedocOutput); + this._lookForMissingReferenceExportsThrowIfExists(referenceNames); + } + + // Some of our packages re-export external package exports in their index.ts + // Typedoc is incapable of rendering these packages, so we need to special-case them + const externalExportToLink: { [externalExport: string]: string } = {}; + const externalExportsWithoutLinks: string[] = []; + const externalExports: string[] = this._getAllExternalExports(); + _.each(externalExports, externalExport => { + const linkIfExists = docGenConfigs.EXTERNAL_EXPORT_TO_LINK[externalExport]; + if (_.isUndefined(linkIfExists)) { + externalExportsWithoutLinks.push(externalExport); + return; + } + externalExportToLink[externalExport] = linkIfExists; + }); + if (!_.isEmpty(externalExportsWithoutLinks)) { + throw new Error( + `Found the following external exports in ${ + this._packageName + }'s index.ts:\n ${externalExportsWithoutLinks.join( + '\n', + )}\nThey are missing from the EXTERNAL_EXPORT_TO_LINK mapping. Add them and try again.`, + ); + } + + const exportPathToTypedocNames: ExportNameToTypedocNames = {}; + _.each(modifiedTypedocOutput.children, file => { + const exportPath = this._findExportPathGivenTypedocName(file.name); + exportPathToTypedocNames[exportPath] = _.isUndefined(exportPathToTypedocNames[exportPath]) + ? [file.name] + : [...exportPathToTypedocNames[exportPath], file.name]; + }); + + // Since we need additional metadata included in the doc JSON, we nest the TypeDoc JSON + // within our own custom, versioned docsJson format. + const docJson = { + version: docGenConfigs.DOC_JSON_VERSION, + metadata: { + exportPathToTypedocNames, + exportPathOrder: this._exportPathOrder, + externalTypeToLink: docGenConfigs.EXTERNAL_TYPE_TO_LINK, + externalExportToLink, + }, + typedocJson: modifiedTypedocOutput, + }; + utils.log(`GENERATE_UPLOAD_DOCS: Saving Doc JSON to: ${jsonFilePath}`); + writeFileSync(jsonFilePath, JSON.stringify(docJson, null, 2)); + + if (this._shouldUploadDocs) { + await this._uploadDocsAsync(jsonFilePath, cwd); + } + utils.log(`GENERATE_UPLOAD_DOCS: Doc generation done for ${this._packageName}`); + } + private async _uploadDocsAsync(jsonFilePath: string, cwd: string): Promise<void> { + const fileName = `v${this._packageJson.version}.json`; + utils.log(`GENERATE_UPLOAD_DOCS: Doc generation successful, uploading docs... as ${fileName}`); + + const S3BucketPath = this._isStaging + ? `s3://staging-doc-jsons/${this._packageName}/` + : `s3://doc-jsons/${this._packageName}/`; + const s3Url = `${S3BucketPath}${fileName}`; + await execAsync( + `aws s3 cp ${jsonFilePath} ${s3Url} --profile 0xproject --grants read=uri=http://acs.amazonaws.com/groups/global/AllUsers --content-type application/json`, + { + cwd, + }, + ); + utils.log(`GENERATE_UPLOAD_DOCS: Docs uploaded to S3 bucket: ${S3BucketPath}`); + // Remove the generated docs directory + await execAsync(`rm -rf ${jsonFilePath}`, { + cwd, + }); + } + /** + * Look for types that are used by the public interface but are missing from a package's index.ts + */ + private _lookForMissingReferenceExportsThrowIfExists(referenceNames: string[]): void { + const allExportedItems = _.flatten(_.values(this._exportPathToExportedItems)); + const missingReferences: string[] = []; + _.each(referenceNames, referenceName => { + if ( + !_.includes(allExportedItems, referenceName) && + _.isUndefined(docGenConfigs.EXTERNAL_TYPE_TO_LINK[referenceName]) + ) { + missingReferences.push(referenceName); + } + }); + if (!_.isEmpty(missingReferences)) { + throw new Error( + `${this._packageName} package needs to export: \n${missingReferences.join( + '\n', + )} \nFrom it\'s index.ts. If any are from external dependencies, then add them to the EXTERNAL_TYPE_TO_LINK mapping.`, + ); + } + } + /** + * Look for exported types that are not used by the package's public interface + */ + private _lookForUnusedExportedTypesThrowIfExists(referenceNames: string[], typedocOutput: any): void { + const exportedTypes = DocGenerateAndUploadUtils._getAllTypeNames(typedocOutput, []); + const excessiveReferences = _.difference(exportedTypes, referenceNames); + const excessiveReferencesExceptIgnored = _.difference( + excessiveReferences, + docGenConfigs.IGNORED_EXCESSIVE_TYPES, + ); + if (!_.isEmpty(excessiveReferencesExceptIgnored)) { + throw new Error( + `${this._packageName} package exports BUT does not need: \n${excessiveReferencesExceptIgnored.join( + '\n', + )} \nin it\'s index.ts. Remove them then try again.`, + ); + } + } + /** + * For each entry in the TypeDoc JSON, remove it if: + * - it was not exported in index.ts + * - the constructor is to be ignored + * - it begins with an underscore (i.e is private) + */ + private _pruneTypedocOutput(typedocOutput: any): any { + const modifiedTypedocOutput = _.cloneDeep(typedocOutput); + _.each(typedocOutput.children, (file, i) => { + const exportPath = this._findExportPathGivenTypedocName(file.name); + const exportItems = this._exportPathToExportedItems[exportPath]; + _.each(file.children, (child, j) => { + const isNotExported = !_.includes(exportItems, child.name); + if (isNotExported) { + delete modifiedTypedocOutput.children[i].children[j]; + return; + } + + const innerChildren = typedocOutput.children[i].children[j].children; + _.each(innerChildren, (innerChild, k) => { + const isHiddenConstructor = + child.kindString === 'Class' && + _.includes(docGenConfigs.CLASSES_WITH_HIDDEN_CONSTRUCTORS, child.name) && + innerChild.kindString === 'Constructor'; + const isPrivate = _.startsWith(innerChild.name, '_'); + if (isHiddenConstructor || isPrivate) { + delete modifiedTypedocOutput.children[i].children[j].children[k]; + } + }); + modifiedTypedocOutput.children[i].children[j].children = _.compact( + modifiedTypedocOutput.children[i].children[j].children, + ); + }); + modifiedTypedocOutput.children[i].children = _.compact(modifiedTypedocOutput.children[i].children); + }); + return modifiedTypedocOutput; + } + /** + * Unfortunately TypeDoc children names will only be prefixed with the name of the package _if_ we passed + * TypeDoc files outside of the packages root path (i.e this package exports another package from our + * monorepo). In order to enforce that the names are always prefixed with the package's name, we check and add + * them here when necessary. + */ + private _standardizeTypedocOutputTopLevelChildNames(typedocOutput: any): any { + const modifiedTypedocOutput = _.cloneDeep(typedocOutput); + _.each(typedocOutput.children, (child, i) => { + if (!_.includes(child.name, '/src/')) { + const nameWithoutQuotes = child.name.replace(/"/g, ''); + const standardizedName = `"${this._packageName}/src/${nameWithoutQuotes}"`; + modifiedTypedocOutput.children[i].name = standardizedName; + } + }); + return modifiedTypedocOutput; + } + /** + * Maps back each top-level TypeDoc JSON object name to the exportPath from which it was generated. + */ + private _findExportPathGivenTypedocName(typedocName: string): string { + const typeDocNameWithoutQuotes = _.replace(typedocName, /"/g, ''); + const sanitizedExportPathToExportPath: { [sanitizedName: string]: string } = {}; + const exportPaths = _.keys(this._exportPathToExportedItems); + const sanitizedExportPaths = _.map(exportPaths, exportPath => { + if (_.startsWith(exportPath, './')) { + const sanitizedExportPath = path.join(this._packageName, 'src', exportPath); + sanitizedExportPathToExportPath[sanitizedExportPath] = exportPath; + return sanitizedExportPath; + } + const monorepoPrefix = '@0xproject/'; + if (_.startsWith(exportPath, monorepoPrefix)) { + const sanitizedExportPath = exportPath.split(monorepoPrefix)[1]; + sanitizedExportPathToExportPath[sanitizedExportPath] = exportPath; + return sanitizedExportPath; + } + sanitizedExportPathToExportPath[exportPath] = exportPath; + return exportPath; + }); + // We need to sort the exportPaths by length (longest first), so that the match finding will pick + // longer matches before shorter matches, since it might match both, but the longer match is more + // precisely what we are looking for. + const sanitizedExportPathsSortedByLength = sanitizedExportPaths.sort((a: string, b: string) => { + return b.length - a.length; + }); + const matchingSanitizedExportPathIfExists = _.find(sanitizedExportPathsSortedByLength, p => { + return _.startsWith(typeDocNameWithoutQuotes, p); + }); + if (_.isUndefined(matchingSanitizedExportPathIfExists)) { + throw new Error(`Didn't find an exportPath for ${typeDocNameWithoutQuotes}`); + } + const matchingExportPath = sanitizedExportPathToExportPath[matchingSanitizedExportPathIfExists]; + return matchingExportPath; + } + private _getAllExternalExports(): string[] { + const externalExports: string[] = []; + _.each(this._exportPathToExportedItems, (exportedItems, exportPath) => { + const pathIfExists = this._monoRepoPkgNameToPath[exportPath]; + if (_.isUndefined(pathIfExists) && !_.startsWith(exportPath, './')) { + _.each(exportedItems, exportedItem => { + externalExports.push(exportedItem); + }); + return; // It's an external package + } + }); + return externalExports; + } + private _getTypeDocFileIncludesForPackage(): string[] { + let typeDocExtraFileIncludes: string[] = []; + _.each(this._exportPathToExportedItems, (exportedItems, exportPath) => { + const isInternalToPkg = _.startsWith(exportPath, '.'); + if (isInternalToPkg) { + const pathToInternalPkg = path.join(this._packagePath, 'src', `${exportPath}.ts`); + typeDocExtraFileIncludes.push(pathToInternalPkg); + return; + } + + const pathIfExists = this._monoRepoPkgNameToPath[exportPath]; + if (_.isUndefined(pathIfExists)) { + return; // It's an external package + } + + const typeDocSourceIncludes = new Set(); + const pathToIndex = `${pathIfExists}/src/index.ts`; + const exportInfo = DocGenerateAndUploadUtils._getExportPathToExportedItems(pathToIndex); + const innerExportPathToExportedItems = exportInfo.exportPathToExportedItems; + _.each(exportedItems, exportName => { + _.each(innerExportPathToExportedItems, (innerExportItems, innerExportPath) => { + if (!_.includes(innerExportItems, exportName)) { + return; + } + if (!_.startsWith(innerExportPath, './')) { + throw new Error( + `GENERATE_UPLOAD_DOCS: WARNING - ${ + this._packageName + } is exporting one of ${innerExportItems} which is + itself exported from an external package. To fix this, export the external dependency directly, + not indirectly through ${innerExportPath}.`, + ); + } else { + const absoluteSrcPath = path.join(pathIfExists, 'src', `${innerExportPath}.ts`); + typeDocSourceIncludes.add(absoluteSrcPath); + } + }); + }); + + // @0xproject/types & ethereum-types are examples of packages where their index.ts exports types + // directly, meaning no internal paths will exist to follow. Other packages also have direct exports + // in their index.ts, so we always add it to the source files passed to TypeDoc + if (typeDocSourceIncludes.size === 0) { + typeDocSourceIncludes.add(pathToIndex); + } + + typeDocExtraFileIncludes = [...typeDocExtraFileIncludes, ...Array.from(typeDocSourceIncludes)]; + }); + return typeDocExtraFileIncludes; + } +} diff --git a/packages/monorepo-scripts/src/utils/github_release_utils.ts b/packages/monorepo-scripts/src/utils/github_release_utils.ts new file mode 100644 index 000000000..28dce70ac --- /dev/null +++ b/packages/monorepo-scripts/src/utils/github_release_utils.ts @@ -0,0 +1,104 @@ +import * as promisify from 'es6-promisify'; +import { readFileSync } from 'fs'; +import * as _ from 'lodash'; +import * as path from 'path'; +import { exec as execAsync } from 'promisify-child-process'; +import * as publishRelease from 'publish-release'; + +import { constants } from '../constants'; +import { Package } from '../types'; + +import { utils } from './utils'; + +const publishReleaseAsync = promisify(publishRelease); +// tslint:disable-next-line:completed-docs +export async function publishReleaseNotesAsync(updatedPublishPackages: Package[]): Promise<void> { + // Git push a tag representing this publish (publish-{commit-hash}) (truncate hash) + const result = await execAsync('git log -n 1 --pretty=format:"%H"', { cwd: constants.monorepoRootPath }); + const latestGitCommit = result.stdout; + const prefixLength = 7; + const shortenedGitCommit = latestGitCommit.slice(0, prefixLength); + const tagName = `monorepo@${shortenedGitCommit}`; + + await execAsync(`git rev-parse ${tagName}`); + await execAsync(`git tag ${tagName}`); + + await execAsync(`git push origin ${tagName}`); + const releaseName = `0x monorepo - ${shortenedGitCommit}`; + + let assets: string[] = []; + let aggregateNotes = ''; + _.each(updatedPublishPackages, pkg => { + const notes = getReleaseNotesForPackage(pkg.packageJson.name, pkg.packageJson.version); + if (_.isEmpty(notes)) { + return; // don't include it + } + aggregateNotes += `### ${pkg.packageJson.name}@${pkg.packageJson.version}\n${notes}\n\n`; + + const packageAssets = _.get(pkg.packageJson, 'config.postpublish.assets'); + if (!_.isUndefined(packageAssets)) { + assets = [...assets, ...packageAssets]; + } + }); + const finalAssets = adjustAssetPaths(assets); + + utils.log('Publishing release notes ', releaseName, '...'); + // TODO: Currently publish-release doesn't let you specify the labels for each asset uploaded + // Ideally we would like to name the assets after the package they are from + // Source: https://github.com/remixz/publish-release/issues/39 + await publishReleaseAsync({ + token: constants.githubPersonalAccessToken, + owner: '0xProject', + tag: tagName, + repo: '0x-monorepo', + name: releaseName, + notes: aggregateNotes, + draft: false, + prerelease: false, + reuseRelease: true, + reuseDraftOnly: false, + assets: finalAssets, + }); +} + +// Asset paths should described from the monorepo root. This method prefixes +// the supplied path with the absolute path to the monorepo root. +function adjustAssetPaths(assets: string[]): string[] { + const finalAssets: string[] = []; + _.each(assets, (asset: string) => { + const finalAsset = `${constants.monorepoRootPath}/${asset}`; + finalAssets.push(finalAsset); + }); + return finalAssets; +} + +function getReleaseNotesForPackage(packageName: string, version: string): string { + const packageNameWithoutNamespace = packageName.replace('@0xproject/', ''); + const changelogJSONPath = path.join( + constants.monorepoRootPath, + 'packages', + packageNameWithoutNamespace, + 'CHANGELOG.json', + ); + const changelogJSON = readFileSync(changelogJSONPath, 'utf-8'); + const changelogs = JSON.parse(changelogJSON); + const latestLog = changelogs[0]; + // If only has a `Dependencies updated` changelog, we don't include it in release notes + if (latestLog.changes.length === 1 && latestLog.changes[0].note === constants.dependenciesUpdatedMessage) { + return ''; + } + // We sanity check that the version for the changelog notes we are about to publish to Github + // correspond to the new version of the package. + // if (version !== latestLog.version) { + // throw new Error('Expected CHANGELOG.json latest entry version to coincide with published version.'); + // } + let notes = ''; + _.each(latestLog.changes, change => { + notes += `* ${change.note}`; + if (change.pr) { + notes += ` (#${change.pr})`; + } + notes += `\n`; + }); + return notes; +} diff --git a/packages/monorepo-scripts/src/utils/utils.ts b/packages/monorepo-scripts/src/utils/utils.ts index 26ac801bd..2ce36942c 100644 --- a/packages/monorepo-scripts/src/utils/utils.ts +++ b/packages/monorepo-scripts/src/utils/utils.ts @@ -48,7 +48,7 @@ export const utils = { }; packages.push(pkg); } catch (err) { - utils.log(`Couldn't find a 'package.json' for ${subpackageName}. Skipping.`); + // Couldn't find a 'package.json' for package. Skipping. } } } |