import * as fs from 'fs' ; import * as path from 'path' ; import * as caseUtils from 'case' ; import * as glob from 'glob' ; import * as semver from 'semver' ; import { LICENSE , NOTICE } from './licensing' ; import { PackageJson , ValidationRule } from './packagejson' ; import { cfnOnlyReadmeContents } from './readme-contents' ; import { deepGet , deepSet , expectDevDependency , expectJSON , fileShouldBe , fileShouldBeginWith , fileShouldContain , fileShouldNotContain , findInnerPackages , monoRepoRoot , } from './util' ; const PKGLINT_VERSION = require ( '../package.json' ) . version ; const AWS_SERVICE_NAMES = require ( './aws-service-official-names.json' ) ; /**
 * Verify that the package name matches the directory name
 */ export class PackageNameMatchesDirectoryName extends ValidationRule { public readonly name = 'naming/package-matches-directory' ; public validate ( pkg ) { const parts = pkg . packageRoot . split ( path . sep ) ; const expectedName = parts [ parts . length - 2 ] . startsWith ( '@' ) ? parts . slice ( parts . length - 2 ) . join ( '/' ) : parts [ parts . length - 1 ] ; expectJSON ( this . name , pkg , 'name' , expectedName ) ; } } /**
 * Verify that all packages have a description
 */ export class DescriptionIsRequired extends ValidationRule { public readonly name = 'package-info/require-description' ; public validate ( pkg ) { if ( ! pkg . json . description ) { pkg . report ( { ruleName : this . name , message : 'Description is required' } ) ; } } } /**
 * Verify that all packages have a publishConfig with a publish tag set.
 */ export class PublishConfigTagIsRequired extends ValidationRule { public readonly name = 'package-info/publish-config-tag' ; private readonly SHARED_PACKAGES = [ '@aws-cdk/assert' , '@aws-cdk/cloud-assembly-schema' , '@aws-cdk/cloudformation-diff' , '@aws-cdk/cx-api' , '@aws-cdk/region-info' , 'aws-cdk' , 'awslint' , 'cdk-assets' , ] ; public validate ( pkg ) { if ( pkg . json . private ) { return ; } const defaultPublishTag = ( cdkMajorVersion ( ) === 2 || ! this . SHARED_PACKAGES . includes ( pkg . packageName ) ) ? 'latest' : 'latest-1' ; if ( pkg . json . publishConfig ?. tag !== defaultPublishTag ) { pkg . report ( { ruleName : this . name , message : `publishConfig.tag must be ${ defaultPublishTag }` , fix : ( ( ) => { const publishConfig = pkg . json . publishConfig ?? { } ; publishConfig . tag = defaultPublishTag ; pkg . json . publishConfig = publishConfig ; } ) , } ) ; } } } /**
 * Verify cdk.out directory is included in npmignore since we should not be
 * publishing it.
 */ export class CdkOutMustBeNpmIgnored extends ValidationRule { public readonly name = 'package-info/npm-ignore-cdk-out' ; public validate ( pkg ) { const npmIgnorePath = path . join ( pkg . packageRoot , '.npmignore' ) ; if ( fs . existsSync ( npmIgnorePath ) ) { const npmIgnore = fs . readFileSync ( npmIgnorePath ) ; if ( ! npmIgnore . includes ( '**/cdk.out' ) ) { pkg . report ( { ruleName : this . name , message : `${ npmIgnorePath }: Must exclude **/cdk.out` , fix : ( ) => fs . writeFileSync ( npmIgnorePath , `${ npmIgnore }\n# exclude cdk artifacts\n**/cdk.out` ) , } ) ; } } } } /**
 * Repository must be our GitHub repo
 */ export class RepositoryCorrect extends ValidationRule { public readonly name = 'package-info/repository' ; public validate ( pkg ) { expectJSON ( this . name , pkg , 'repository.type' , 'git' ) ; expectJSON ( this . name , pkg , 'repository.url' , 'https://github.com/aws/aws-cdk.git' ) ; const pkgDir = path . relative ( monoRepoRoot ( ) , pkg . packageRoot ) ; expectJSON ( this . name , pkg , 'repository.directory' , pkgDir ) ; } } /**
 * Homepage must point to the GitHub repository page.
 */ export class HomepageCorrect extends ValidationRule { public readonly name = 'package-info/homepage' ; public validate ( pkg ) { expectJSON ( this . name , pkg , 'homepage' , 'https://github.com/aws/aws-cdk' ) ; } } /**
 * The license must be Apache-2.0.
 */ export class License extends ValidationRule { public readonly name = 'package-info/license' ; public validate ( pkg ) { expectJSON ( this . name , pkg , 'license' , 'Apache-2.0' ) ; } } /**
 * There must be a license file that corresponds to the Apache-2.0 license.
 */ export class LicenseFile extends ValidationRule { public readonly name = 'license/license-file' ; public validate ( pkg ) { fileShouldBe ( this . name , pkg , 'LICENSE' , LICENSE ) ; } } /**
 * There must be a NOTICE file.
 */ export class NoticeFile extends ValidationRule { public readonly name = 'license/notice-file' ; public validate ( pkg ) { fileShouldBeginWith ( this . name , pkg , 'NOTICE' , ... NOTICE . split ( '\n' ) ) ; } } /**
 * NOTICE files must contain 3rd party attributions
 */ export class ThirdPartyAttributions extends ValidationRule { public readonly name = 'license/3p-attributions' ; public validate ( pkg ) { const alwaysCheck = [ 'monocdk' , 'aws-cdk-lib' ] ; if ( pkg . json . private && ! alwaysCheck . includes ( pkg . json . name ) ) { return ; } const bundled = pkg . getAllBundledDependencies ( ) . filter ( dep => ! dep . startsWith ( '@aws-cdk' ) ) ; const attribution = pkg . json . pkglint ?. attribution ?? [ ] ; const noticePath = path . join ( pkg . packageRoot , 'NOTICE' ) ; const lines = fs . existsSync ( noticePath ) ? fs . readFileSync ( noticePath , { encoding : 'utf8' } ) . split ( '\n' ) : [ ] ; const re = /^\*\* (\S+)/ ; const attributions = lines . filter ( l => re . test ( l ) ) . map ( l => l . match ( re ) ! [ 1 ] ) ; for ( const dep of bundled ) { if ( ! attributions . includes ( dep ) ) { pkg . report ( { message : `Missing attribution for bundled dependency '${ dep }' in NOTICE file.` , ruleName : this . name , } ) ; } } for ( const attr of attributions ) { if ( ! bundled . includes ( attr ) && ! attribution . includes ( attr ) ) { pkg . report ( { message : `Unnecessary attribution found for dependency '${ attr }' in NOTICE file. Attribution is determined from package.json (all "bundledDependencies" or the list in "pkglint.attribution")` , ruleName : this . name , } ) ; } } } } /**
 * Author must be AWS (as an Organization)
 */ export class AuthorAWS extends ValidationRule { public readonly name = 'package-info/author' ; public validate ( pkg ) { expectJSON ( this . name , pkg , 'author.name' , 'Amazon Web Services' ) ; expectJSON ( this . name , pkg , 'author.url' , 'https://aws.amazon.com' ) ; expectJSON ( this . name , pkg , 'author.organization' , true ) ; } } /**
 * There must be a README.md file.
 */ export class ReadmeFile extends ValidationRule { public readonly name = 'package-info/README.md' ; public validate ( pkg ) { const readmeFile = path . join ( pkg . packageRoot , 'README.md' ) ; const scopes = pkg . json [ 'cdk-build' ] && pkg . json [ 'cdk-build' ] . cloudformation ; if ( ! scopes ) { return ; } if ( pkg . packageName === '@aws-cdk/core' ) { return ; } const scope = typeof scopes === 'string' ? scopes : scopes [ 0 ] ; const serviceName = AWS_SERVICE_NAMES [ scope ] ; if ( pkg . json . maturity === 'cfn-only' ) { fileShouldBe ( this . name , pkg , 'README.md' , cfnOnlyReadmeContents ( { cfnNamespace : scope , packageName : pkg . packageName , } ) ) ; return ; } const disclaimerRegex = beginEndRegex ( 'CFNONLY DISCLAIMER' ) ; const currentReadme = readIfExists ( readmeFile ) ; if ( currentReadme && disclaimerRegex . test ( currentReadme ) ) { pkg . report ( { ruleName : this . name , message : 'README must not include CFNONLY DISCLAIMER section' , fix : ( ) => fs . writeFileSync ( readmeFile , currentReadme . replace ( disclaimerRegex , '' ) ) , } ) ; } const headline = serviceName && `${ serviceName } Construct Library` ; if ( ! fs . existsSync ( readmeFile ) ) { pkg . report ( { ruleName : this . name , message : 'There must be a README.md file at the root of the package' , fix : ( ) => fs . writeFileSync ( readmeFile , [ `# ${ headline || pkg . json . description }` , 'This module is part of the[AWS Cloud Development Kit](https://github.com/aws/aws-cdk) project.' , ] . join ( '\n' ) ) , } ) ; } else if ( headline ) { const requiredFirstLine = `# ${ headline }` ; const [ firstLine , ... rest ] = fs . readFileSync ( readmeFile , { encoding : 'utf8' } ) . split ( '\n' ) ; if ( firstLine !== requiredFirstLine ) { pkg . report ( { ruleName : this . name , message : `The title of the README.md file must be "${ headline }"` , fix : ( ) => fs . writeFileSync ( readmeFile , [ requiredFirstLine , ... rest ] . join ( '\n' ) ) , } ) ; } } } } /**
 * All packages must have a "maturity" declaration.
 *
 * The banner in the README must match the package maturity.
 *
 * As a way to seed the settings, if 'maturity' is missing but can
 * be auto-derived from 'stability', that will be the fix (otherwise
 * there is no fix).
 */ export class MaturitySetting extends ValidationRule { public readonly name = 'package-info/maturity' ; public validate ( pkg ) { if ( pkg . json . private ) { return ; } if ( pkg . json . features ) { return ; } let maturity = pkg . json . maturity as string | undefined ; const stability = pkg . json . stability as string | undefined ; if ( ! maturity ) { let fix ; if ( stability && [ 'stable' , 'deprecated' ] . includes ( stability ) ) { fix = ( ) => pkg . json . maturity = stability ; maturity = stability ; } pkg . report ( { ruleName : this . name , message : `Package is missing "maturity" setting (expected one of ${ Object . keys ( MATURITY_TO_STABILITY ) })` , fix , } ) ; } if ( pkg . json . deprecated && maturity !== 'deprecated' ) { pkg . report ( { ruleName : this . name , message : `Package is deprecated, but is marked with maturity "${ maturity }"` , fix : ( ) => pkg . json . maturity = 'deprecated' , } ) ; maturity = 'deprecated' ; } const packageLevels = this . determinePackageLevels ( pkg ) ; const hasL1s = packageLevels . some ( level => level === 'l1' ) ; const hasL2s = packageLevels . some ( level => level === 'l2' ) ; if ( hasL2s ) { if ( maturity === 'cfn-only' ) { pkg . report ( { ruleName : this . name , message : "Package that contains any L2s cannot declare a 'cfn-only' maturity" , fix : ( ) => pkg . json . maturity = 'experimental' , } ) ; } } else if ( hasL1s ) { if ( maturity !== 'cfn-only' ) { pkg . report ( { ruleName : this . name , message : "Package that contains only L1s cannot declare a maturity other than 'cfn-only'" , fix : ( ) => pkg . json . maturity = 'cfn-only' , } ) ; } } if ( maturity ) { this . validateReadmeHasBanner ( pkg , maturity , packageLevels ) ; } } private validateReadmeHasBanner ( pkg , maturity , levelsPresent ) { const badge = this . readmeBadge ( maturity , levelsPresent ) ; if ( ! badge ) { return ; } const readmeFile = path . join ( pkg . packageRoot , 'README.md' ) ; if ( ! fs . existsSync ( readmeFile ) ) { return ; } const readmeContent = fs . readFileSync ( readmeFile , { encoding : 'utf8' } ) ; const badgeRegex = toRegExp ( badge ) ; if ( ! badgeRegex . test ( readmeContent ) ) { const [ title , ... body ] = readmeContent . replace ( /<!--BEGIN STABILITY BANNER-->(?:.|\n)+<!--END STABILITY BANNER-->\n+/m , '' ) . split ( '\n' ) ; pkg . report ( { ruleName : this . name , message : `Missing stability banner for ${ maturity } in README.md file` , fix : ( ) => fs . writeFileSync ( readmeFile , [ title , badge , ... body ] . join ( '\n' ) ) , } ) ; } } private readmeBadge ( maturity , levelsPresent ) { const bannerContents = levelsPresent . map ( level => fs . readFileSync ( path . join ( __dirname , 'banners' , `${ level }.${ maturity }.md` ) , { encoding : 'utf-8' } ) . trim ( ) ) . join ( '\n\n' ) . trim ( ) ; const bannerLines = bannerContents . split ( '\n' ) . map ( s => s . trimRight ( ) ) ; return [ '<!--BEGIN STABILITY BANNER-->' , '' , '---' , '' , ... bannerLines , '' , '---' , '' , '<!--END STABILITY BANNER-->' , '' , ] . join ( '\n' ) ; } private determinePackageLevels ( pkg ) { const hasL1 = ! ! pkg . json [ 'cdk-build' ] ?. cloudformation ; const libFiles = glob . sync ( 'lib/**/*.ts' , { ignore : 'lib/**/*.d.ts' , } ) ; const hasL2 = libFiles . some ( f => ! f . endsWith ( '.generated.ts' ) && ! f . endsWith ( 'index.ts' ) ) ; return [ ... hasL1 ? [ 'l1' ] : [ ] , ... hasL2 || ! hasL1 ? [ 'l2' ] : [ ] , ] ; } } const MATURITY_TO_STABILITY = { 'cfn-only' : 'experimental' , 'experimental' : 'experimental' , 'developer-preview' : 'experimental' , 'stable' : 'stable' , 'deprecated' : 'deprecated' , } ; /**
 * There must be a stability setting, and it must match the package maturity.
 *
 * Maturity setting is leading here (as there are more options than the
 * stability setting), but the stability setting must be present for `jsii`
 * to properly read and encode it into the assembly.
 */ export class StabilitySetting extends ValidationRule { public readonly name = 'package-info/stability' ; public validate ( pkg ) { if ( pkg . json . private ) { return ; } if ( pkg . json . features ) { return ; } const maturity = pkg . json . maturity as string | undefined ; const stability = pkg . json . stability as string | undefined ; const expectedStability = maturity ? MATURITY_TO_STABILITY [ maturity ] : undefined ; if ( ! stability || ( expectedStability && stability !== expectedStability ) ) { pkg . report ( { ruleName : this . name , message : `stability is '${ stability }', but based on maturity is expected to be '${ expectedStability }'` , fix : expectedStability ? ( ( ) => pkg . json . stability = expectedStability ) : undefined , } ) ; } } } export class FeatureStabilityRule extends ValidationRule { public readonly name = 'package-info/feature-stability' ; private readonly badges : { [ key ] : string ; } = { 'Not Implemented' : 'https://img.shields.io/badge/not--implemented-black.svg?style=for-the-badge' , 'Experimental' : 'https://img.shields.io/badge/experimental-important.svg?style=for-the-badge' , 'Developer Preview' : 'https://img.shields.io/badge/developer--preview-informational.svg?style=for-the-badge' , 'Stable' : 'https://img.shields.io/badge/stable-success.svg?style=for-the-badge' , } ; public validate ( pkg ) { if ( pkg . json . private || ! pkg . json . features ) { return ; } const featuresColumnWitdh = Math . max ( 13 , ... pkg . json . features . map ( ( feat ) => feat . name . length ) ) ; const stabilityBanner = [ '<!--BEGIN STABILITY BANNER-->' , '' , '---' , '' , `Features${ ' ' . repeat ( featuresColumnWitdh - 8 ) } | Stability` , `--------${ '-' . repeat ( featuresColumnWitdh - 8 ) }-|-----------${ '-' . repeat ( Math . max ( 0 , 100 - featuresColumnWitdh - 13 ) ) }` , ... this . featureEntries ( pkg , featuresColumnWitdh ) , '' , ... this . bannerNotices ( pkg ) , '---' , '' , '<!--END STABILITY BANNER-->' , '' , ] . join ( '\n' ) ; const readmeFile = path . join ( pkg . packageRoot , 'README.md' ) ; if ( ! fs . existsSync ( readmeFile ) ) { return ; } const readmeContent = fs . readFileSync ( readmeFile , { encoding : 'utf8' } ) ; const stabilityRegex = toRegExp ( stabilityBanner ) ; if ( ! stabilityRegex . test ( readmeContent ) ) { const [ title , ... body ] = readmeContent . replace ( /<!--BEGIN STABILITY BANNER-->(?:.|\n)+<!--END STABILITY BANNER-->\n+/m , '' ) . split ( '\n' ) ; pkg . report ( { ruleName : this . name , message : 'Stability banner does not match as expected' , fix : ( ) => fs . writeFileSync ( readmeFile , [ title , stabilityBanner , ... body ] . join ( '\n' ) ) , } ) ; } } private featureEntries ( pkg , featuresColumnWitdh ) { const entries = [ ] ; if ( pkg . json [ 'cdk-build' ] ?. cloudformation ) { entries . push ( `CFN Resources${ ' ' . repeat ( featuresColumnWitdh - 13 ) } | ![Stable](${ this . badges . Stable })` ) ; } pkg . json . features . forEach ( ( feature ) => { const badge = this . badges [ feature . stability ] ; if ( ! badge ) { throw new Error ( `Unknown stability - ${ feature . stability }` ) ; } entries . push ( `${ feature . name }${ ' ' . repeat ( featuresColumnWitdh - feature . name . length ) } | ![${ feature . stability }](${ badge })` ) ; } ) ; return entries ; } private bannerNotices ( pkg ) { const notices = [ ] ; if ( pkg . json [ 'cdk-build' ] ?. cloudformation ) { notices . push ( readBannerFile ( 'features-cfn-stable.md' ) ) ; notices . push ( '' ) ; } const noticeOrder = [ 'Experimental' , 'Developer Preview' , 'Stable' ] ; const stabilities = pkg . json . features . map ( ( f ) => f . stability ) ; const filteredNotices = noticeOrder . filter ( v => stabilities . includes ( v ) ) ; for ( const notice of filteredNotices ) { if ( notices . length !== 0 ) { notices . push ( '<!-- -->' , '' ) ; } const lowerTrainCase = notice . toLowerCase ( ) . replace ( /\s/g , '-' ) ; notices . push ( readBannerFile ( `features-${ lowerTrainCase }.md` ) ) ; notices . push ( '' ) ; } return notices ; } } /**
 * Keywords must contain CDK keywords and be sorted
 */ export class CDKKeywords extends ValidationRule { public readonly name = 'package-info/keywords' ; public validate ( pkg ) { if ( ! pkg . json . keywords ) { pkg . report ( { ruleName : this . name , message : 'Must have keywords' , fix : ( ) => { pkg . json . keywords = [ ] ; } , } ) ; } const keywords = pkg . json . keywords || [ ] ; if ( keywords . indexOf ( 'cdk' ) === - 1 ) { pkg . report ( { ruleName : this . name , message : 'Keywords must mention CDK' , fix : ( ) => { pkg . json . keywords . splice ( 0 , 0 , 'cdk' ) ; } , } ) ; } if ( keywords . indexOf ( 'aws' ) === - 1 ) { pkg . report ( { ruleName : this . name , message : 'Keywords must mention AWS' , fix : ( ) => { pkg . json . keywords . splice ( 0 , 0 , 'aws' ) ; } , } ) ; } } } /**
 * Requires projectReferences to be set in the jsii configuration.
 */ export class JSIIProjectReferences extends ValidationRule { public readonly name = 'jsii/project-references' ; public validate ( pkg ) { if ( ! isJSII ( pkg ) ) { return ; } expectJSON ( this . name , pkg , 'jsii.projectReferences' , pkg . json . name !== 'monocdk' && pkg . json . name !== 'aws-cdk-lib' ) ; } } export class NoPeerDependenciesMonocdk extends ValidationRule { public readonly name = 'monocdk/no-peer' ; private readonly allowedPeer = [ 'constructs' ] ; private readonly modules = [ 'monocdk' , 'aws-cdk-lib' ] ; public validate ( pkg ) { if ( ! this . modules . includes ( pkg . packageName ) ) { return ; } const peers = Object . keys ( pkg . peerDependencies ) . filter ( peer => ! this . allowedPeer . includes ( peer ) ) ; if ( peers . length > 0 ) { pkg . report ( { ruleName : this . name , message : `Adding a peer dependency to the monolithic package ${ pkg . packageName } is a breaking change, and thus not allowed.
         Added ${ peers . join ( ' ' ) }` , } ) ; } } } /**
 * Validates that the same version of `constructs` is used wherever a dependency
 * is specified, so that they must all be udpated at the same time (through an
 * update to this rule).
 *
 * Note: v1 and v2 use different versions respectively.
 */ export class ConstructsVersion extends ValidationRule { public static readonly VERSION = cdkMajorVersion ( ) === 2 ? '10.0.0-pre.5' : '^3.3.69' ; public readonly name = 'deps/constructs' ; public validate ( pkg ) { const toCheck = new Array < string > ( ) ; if ( 'constructs' in pkg . dependencies ) { toCheck . push ( 'dependencies' ) ; } if ( 'constructs' in pkg . devDependencies ) { toCheck . push ( 'devDependencies' ) ; } if ( 'constructs' in pkg . peerDependencies ) { toCheck . push ( 'peerDependencies' ) ; } for ( const cfg of toCheck ) { expectJSON ( this . name , pkg , `${ cfg }.constructs` , ConstructsVersion . VERSION ) ; } } } /**
 * JSII Java package is required and must look sane
 */ export class JSIIJavaPackageIsRequired extends ValidationRule { public readonly name = 'jsii/java' ; public validate ( pkg ) { if ( ! isJSII ( pkg ) ) { return ; } const moduleName = cdkModuleName ( pkg . json . name ) ; expectJSON ( this . name , pkg , 'jsii.targets.java.maven.groupId' , 'software.amazon.awscdk' ) ; expectJSON ( this . name , pkg , 'jsii.targets.java.maven.artifactId' , moduleName . mavenArtifactId , /-/g ) ; const java = deepGet ( pkg . json , [ 'jsii' , 'targets' , 'java' , 'package' ] ) as string | undefined ; expectJSON ( this . name , pkg , 'jsii.targets.java.package' , moduleName . javaPackage , /\./g ) ; if ( java ) { const expectedPrefix = moduleName . javaPackage . split ( '.' ) . slice ( 0 , 3 ) . join ( '.' ) ; const actualPrefix = java . split ( '.' ) . slice ( 0 , 3 ) . join ( '.' ) ; if ( expectedPrefix !== actualPrefix ) { pkg . report ( { ruleName : this . name , message : `JSII "java" package must share the first 3 elements of the expected one: ${ expectedPrefix } vs ${ actualPrefix }` , fix : ( ) => deepSet ( pkg . json , [ 'jsii' , 'targets' , 'java' , 'package' ] , moduleName . javaPackage ) , } ) ; } } } } export class JSIIPythonTarget extends ValidationRule { public readonly name = 'jsii/python' ; public validate ( pkg ) { if ( ! isJSII ( pkg ) ) { return ; } const moduleName = cdkModuleName ( pkg . json . name ) ; expectJSON ( this . name , pkg , 'jsii.targets.python.distName' , moduleName . python . distName ) ; expectJSON ( this . name , pkg , 'jsii.targets.python.module' , moduleName . python . module ) ; expectJSON ( this . name , pkg , 'jsii.targets.python.classifiers' , [ 'Framework :: AWS CDK' , 'Framework :: AWS CDK :: 1' ] ) ; } } export class CDKPackage extends ValidationRule { public readonly name = 'package-info/scripts/package' ; public validate ( pkg ) { if ( pkg . json . private ) { return ; } const merkleMarker = '.LAST_PACKAGE' ; if ( ! shouldUseCDKBuildTools ( pkg ) ) { return ; } expectJSON ( this . name , pkg , 'scripts.package' , 'cdk-package' ) ; const outdir = 'dist' ; if ( isJSII ( pkg ) ) { expectJSON ( this . name , pkg , 'jsii.outdir' , outdir ) ; } fileShouldContain ( this . name , pkg , '.npmignore' , outdir ) ; fileShouldContain ( this . name , pkg , '.gitignore' , outdir ) ; fileShouldContain ( this . name , pkg , '.npmignore' , merkleMarker ) ; fileShouldContain ( this . name , pkg , '.gitignore' , merkleMarker ) ; } } export class NoTsBuildInfo extends ValidationRule { public readonly name = 'npmignore/tsbuildinfo' ; public validate ( pkg ) { if ( pkg . json . private ) { return ; } fileShouldContain ( this . name , pkg , '.npmignore' , '*.tsbuildinfo' ) ; } } export class NoTestsInNpmPackage extends ValidationRule { public readonly name = 'npmignore/test' ; public validate ( pkg ) { if ( pkg . json . private ) { return ; } if ( pkg . packageName === 'aws-cdk' ) { return ; } fileShouldContain ( this . name , pkg , '.npmignore' , 'test/' ) ; } } export class NoTsConfig extends ValidationRule { public readonly name = 'npmignore/tsconfig' ; public validate ( pkg ) { if ( pkg . json . private ) { return ; } fileShouldContain ( this . name , pkg , '.npmignore' , 'tsconfig.json' ) ; } } export class IncludeJsiiInNpmTarball extends ValidationRule { public readonly name = 'npmignore/jsii-included' ; public validate ( pkg ) { if ( ! isJSII ( pkg ) ) { return ; } if ( pkg . json . private ) { return ; } fileShouldNotContain ( this . name , pkg , '.npmignore' , '.jsii' ) ; fileShouldContain ( this . name , pkg , '.npmignore' , '!.jsii' ) ; } } /**
 * Verifies there is no dependency on "jsii" since it's defined at the repo
 * level.
 */ export class NoJsiiDep extends ValidationRule { public readonly name = 'dependencies/no-jsii' ; public validate ( pkg ) { const predicate = ( s ) => s . startsWith ( 'jsii' ) ; if ( pkg . getDevDependency ( predicate ) ) { pkg . report ( { ruleName : this . name , message : 'packages should not have a devDep on jsii since it is defined at the repo level' , fix : ( ) => pkg . removeDevDependency ( predicate ) , } ) ; } } } /**
 * Verifies that the expected versions of node will be supported.
 */ export class NodeCompatibility extends ValidationRule { public readonly name = 'dependencies/node-version' ; public validate ( pkg ) { const atTypesNode = pkg . getDevDependency ( '@types/node' ) ; if ( atTypesNode && ! atTypesNode . startsWith ( '^10.' ) ) { pkg . report ( { ruleName : this . name , message : `packages must support node version 10 and up, but ${ atTypesNode } is declared` , fix : ( ) => pkg . addDevDependency ( '@types/node' , '^10.17.5' ) , } ) ; } } } function isCdkModuleName ( name ) { return ! ! name . match ( /^@aws-cdk\// ) ; } /**
 * Computes the module name for various other purposes (java package, ...)
 */ function cdkModuleName ( name ) { const isCdkPkg = name === '@aws-cdk/core' ; const isLegacyCdkPkg = name === '@aws-cdk/cdk' ; let suffix = name ; suffix = suffix . replace ( /^aws-cdk-/ , '' ) ; suffix = suffix . replace ( /^@aws-cdk\// , '' ) ; const dotnetSuffix = suffix . split ( '-' ) . map ( s => s === 'aws' ? 'AWS' : caseUtils . pascal ( s ) ) . join ( '.' ) ; const pythonName = suffix . replace ( /^@/g , '' ) . replace ( /\//g , '.' ) . split ( '.' ) . map ( caseUtils . kebab ) . join ( '.' ) ; const mavenIdMap = { '@aws-cdk/core' : 'core' , '@aws-cdk/cdk' : 'cdk' , '@aws-cdk/assertions' : 'assertions' , '@aws-cdk/assertions-alpha' : 'assertions-alpha' , } ; const mavenArtifactId = name in mavenIdMap ? mavenIdMap [ name ] : ( suffix . startsWith ( 'aws-' ) || suffix . startsWith ( 'alexa-' ) ) ? suffix . replace ( /aws-/ , '' ) : suffix . startsWith ( 'cdk-' ) ? suffix : `cdk-${ suffix }` ; return { javaPackage : `software.amazon.awscdk${ isLegacyCdkPkg ? '' : `.${ suffix . replace ( /aws-/ , 'services-' ) . replace ( /-/g , '.' ) }` }` , mavenArtifactId , dotnetNamespace : `Amazon.CDK${ isCdkPkg ? '' : `.${ dotnetSuffix }` }` , python : { distName : `aws-cdk.${ pythonName }` , module : `aws_cdk.${ pythonName . replace ( /-/g , '_' ) }` , } , } ; } /**
 * JSII .NET namespace is required and must look sane
 */ export class JSIIDotNetNamespaceIsRequired extends ValidationRule { public readonly name = 'jsii/dotnet' ; public validate ( pkg ) { if ( ! isJSII ( pkg ) ) { return ; } if ( pkg . packageName === '@aws-cdk/cdk' ) { return ; } const dotnet = deepGet ( pkg . json , [ 'jsii' , 'targets' , 'dotnet' , 'namespace' ] ) as string | undefined ; const moduleName = cdkModuleName ( pkg . json . name ) ; expectJSON ( this . name , pkg , 'jsii.targets.dotnet.namespace' , moduleName . dotnetNamespace , /\./g , true ) ; if ( dotnet ) { const actualPrefix = dotnet . split ( '.' ) . slice ( 0 , 2 ) . join ( '.' ) ; const expectedPrefix = moduleName . dotnetNamespace . split ( '.' ) . slice ( 0 , 2 ) . join ( '.' ) ; if ( actualPrefix !== expectedPrefix ) { pkg . report ( { ruleName : this . name , message : `.NET namespace must share the first two segments of the default namespace, '${ expectedPrefix }' vs '${ actualPrefix }'` , fix : ( ) => deepSet ( pkg . json , [ 'jsii' , 'targets' , 'dotnet' , 'namespace' ] , moduleName . dotnetNamespace ) , } ) ; } } } } /**
 * JSII .NET namespace is required and must look sane
 */ export class JSIIDotNetIconUrlIsRequired extends ValidationRule { public readonly name = 'jsii/dotnet/icon-url' ; public validate ( pkg ) { if ( ! isJSII ( pkg ) ) { return ; } const CDK_LOGO_URL = 'https://raw.githubusercontent.com/aws/aws-cdk/master/logo/default-256-dark.png' ; expectJSON ( this . name , pkg , 'jsii.targets.dotnet.iconUrl' , CDK_LOGO_URL ) ; } } /**
 * The package must depend on cdk-build-tools
 */ export class MustDependOnBuildTools extends ValidationRule { public readonly name = 'dependencies/build-tools' ; public validate ( pkg ) { if ( ! shouldUseCDKBuildTools ( pkg ) ) { return ; } expectDevDependency ( this . name , pkg , '@aws-cdk/cdk-build-tools' , `${ PKGLINT_VERSION }` ) ; } } /**
 * Build script must be 'cdk-build'
 */ export class MustUseCDKBuild extends ValidationRule { public readonly name = 'package-info/scripts/build' ; public validate ( pkg ) { if ( ! shouldUseCDKBuildTools ( pkg ) ) { return ; } expectJSON ( this . name , pkg , 'scripts.build' , 'cdk-build' ) ; const merkleMarker = '.LAST_BUILD' ; fileShouldContain ( this . name , pkg , '.gitignore' , merkleMarker ) ; fileShouldContain ( this . name , pkg , '.npmignore' , merkleMarker ) ; } } /**
 * Dependencies in both regular and peerDependencies must agree in semver
 *
 * In particular, verify that depVersion satisfies peerVersion. This prevents
 * us from instructing NPM to construct impossible closures, where we say:
 *
 *    peerDependency: A@1.0.0
 *    dependency: A@2.0.0
 *
 * There is no version of A that would satisfy this.
 *
 * The other way around is not necessary--the depVersion can be bumped without
 * bumping the peerVersion (if the API didn't change this may be perfectly
 * valid). This prevents us from restricting a user's potential combinations of
 * libraries unnecessarily.
 */ export class RegularDependenciesMustSatisfyPeerDependencies extends ValidationRule { public readonly name = 'dependencies/peer-dependencies-satisfied' ; public validate ( pkg ) { for ( const [ depName , peerVersion ] of Object . entries ( pkg . peerDependencies ) ) { const depVersion = pkg . dependencies [ depName ] ; if ( depVersion === undefined ) { continue ; } if ( ! semver . intersects ( depVersion , peerVersion ) ) { pkg . report ( { ruleName : this . name , message : `dependency ${ depName }: concrete version ${ depVersion } does not match peer version '${ peerVersion }'` , fix : ( ) => pkg . addPeerDependency ( depName , depVersion ) , } ) ; } } } } /**
 * Check that dependencies on @aws-cdk/ packages use point versions (not version ranges)
 * and that they are also defined in `peerDependencies`.
 */ export class MustDependonCdkByPointVersions extends ValidationRule { public readonly name = 'dependencies/cdk-point-dependencies' ; public validate ( pkg ) { const expectedVersion = require ( path . join ( monoRepoRoot ( ) , 'package.json' ) ) . version ; const ignore = [ '@aws-cdk/cloudformation-diff' , '@aws-cdk/cfnspec' , '@aws-cdk/cx-api' , '@aws-cdk/cloud-assembly-schema' , '@aws-cdk/region-info' , ... fs . readdirSync ( path . join ( monoRepoRoot ( ) , 'tools' , '@aws-cdk' ) ) . map ( ( name ) => `@aws-cdk/${ name }` ) , ] ; for ( const [ depName , depVersion ] of Object . entries ( pkg . dependencies ) ) { if ( ! isCdkModuleName ( depName ) || ignore . includes ( depName ) ) { continue ; } const peerDep = pkg . peerDependencies [ depName ] ; if ( ! peerDep ) { pkg . report ( { ruleName : this . name , message : `dependency ${ depName } must also appear in peerDependencies` , fix : ( ) => pkg . addPeerDependency ( depName , expectedVersion ) , } ) ; } if ( peerDep !== expectedVersion ) { pkg . report ( { ruleName : this . name , message : `peer dependency ${ depName } should have the version ${ expectedVersion }` , fix : ( ) => pkg . addPeerDependency ( depName , expectedVersion ) , } ) ; } if ( depVersion !== expectedVersion ) { pkg . report ( { ruleName : this . name , message : `dependency ${ depName }: dependency version must be ${ expectedVersion }` , fix : ( ) => pkg . addDependency ( depName , expectedVersion ) , } ) ; } } } } export class MustIgnoreSNK extends ValidationRule { public readonly name = 'ignore/strong-name-key' ; public validate ( pkg ) { fileShouldContain ( this . name , pkg , '.npmignore' , '*.snk' ) ; fileShouldContain ( this . name , pkg , '.gitignore' , '*.snk' ) ; } } export class MustIgnoreJunitXml extends ValidationRule { public readonly name = 'ignore/junit' ; public validate ( pkg ) { fileShouldContain ( this . name , pkg , '.npmignore' , 'junit.xml' ) ; fileShouldContain ( this . name , pkg , '.gitignore' , 'junit.xml' ) ; } } export class NpmIgnoreForJsiiModules extends ValidationRule { public readonly name = 'ignore/jsii' ; public validate ( pkg ) { if ( ! isJSII ( pkg ) ) { return ; } fileShouldContain ( this . name , pkg , '.npmignore' , '*.ts' , '!*.d.ts' , '!*.js' , '!*.lit.ts' , 'coverage' , '.nyc_output' , '*.tgz' ) ; } } /**
 * Must use 'cdk-watch' command
 */ export class MustUseCDKWatch extends ValidationRule { public readonly name = 'package-info/scripts/watch' ; public validate ( pkg ) { if ( ! shouldUseCDKBuildTools ( pkg ) ) { return ; } expectJSON ( this . name , pkg , 'scripts.watch' , 'cdk-watch' ) ; } } /**
 * Must have 'rosetta:extract' command if this package is JSII-enabled.
 */ export class MustHaveRosettaExtract extends ValidationRule { public readonly name = 'package-info/scripts/rosetta:extract' ; public validate ( pkg ) { if ( ! isJSII ( pkg ) ) { return ; } expectJSON ( this . name , pkg , 'scripts.rosetta:extract' , 'yarn --silent jsii-rosetta extract' ) ; } } /**
 * Must use 'cdk-test' command
 */ export class MustUseCDKTest extends ValidationRule { public readonly name = 'package-info/scripts/test' ; public validate ( pkg ) { if ( ! shouldUseCDKBuildTools ( pkg ) ) { return ; } if ( ! hasTestDirectory ( pkg ) ) { return ; } expectJSON ( this . name , pkg , 'scripts.test' , 'cdk-test' ) ; fileShouldContain ( this . name , pkg , '.gitignore' , '.nyc_output' ) ; fileShouldContain ( this . name , pkg , '.gitignore' , 'coverage' ) ; fileShouldContain ( this . name , pkg , '.gitignore' , 'nyc.config.js' ) ; } } /**
 * Must declare minimum node version
 */ export class MustHaveNodeEnginesDeclaration extends ValidationRule { public readonly name = 'package-info/engines' ; public validate ( pkg ) { if ( cdkMajorVersion ( ) === 2 ) { expectJSON ( this . name , pkg , 'engines.node' , '>= 14.15.0' ) ; } else { expectJSON ( this . name , pkg , 'engines.node' , '>= 10.13.0 <13 || >=13.7.0' ) ; } } } /**
 * Scripts that run integ tests must also have the individual 'integ' script to update them
 *
 * This commands comes from the dev-dependency cdk-integ-tools.
 */ export class MustHaveIntegCommand extends ValidationRule { public readonly name = 'package-info/scripts/integ' ; public validate ( pkg ) { if ( ! hasIntegTests ( pkg ) ) { return ; } expectJSON ( this . name , pkg , 'scripts.integ' , 'cdk-integ' ) ; expectDevDependency ( this . name , pkg , '@aws-cdk/cdk-integ-tools' , `${ PKGLINT_VERSION }` ) ; } } /**
 * Checks API backwards compatibility against the latest released version.
 */ export class CompatScript extends ValidationRule { public readonly name = 'package-info/scripts/compat' ; public validate ( pkg ) { if ( ! isJSII ( pkg ) ) { return ; } expectJSON ( this . name , pkg , 'scripts.compat' , 'cdk-compat' ) ; } } export class PkgLintAsScript extends ValidationRule { public readonly name = 'package-info/scripts/pkglint' ; public validate ( pkg ) { const script = 'pkglint -f' ; expectDevDependency ( this . name , pkg , '@aws-cdk/pkglint' , `${ PKGLINT_VERSION }` ) ; if ( ! pkg . npmScript ( 'pkglint' ) ) { pkg . report ( { ruleName : this . name , message : 'a script called "pkglint" must be included to allow fixing package linting issues' , fix : ( ) => pkg . changeNpmScript ( 'pkglint' , ( ) => script ) , } ) ; } if ( pkg . npmScript ( 'pkglint' ) !== script ) { pkg . report ( { ruleName : this . name , message : 'the pkglint script should be: ' + script , fix : ( ) => pkg . changeNpmScript ( 'pkglint' , ( ) => script ) , } ) ; } } } export class NoStarDeps extends ValidationRule { public readonly name = 'dependencies/no-star' ; public validate ( pkg ) { reportStarDeps ( this . name , pkg . json . depedencies ) ; reportStarDeps ( this . name , pkg . json . devDependencies ) ; function reportStarDeps ( ruleName , deps ? ) { deps = deps || { } ; Object . keys ( deps ) . forEach ( d => { if ( deps [ d ] === '*' ) { pkg . report ( { ruleName , message : `star dependency not allowed for ${ d }` , } ) ; } } ) ; } } } interface VersionCount { version : string ; count : number ; } /**
 * All consumed versions of dependencies must be the same
 *
 * NOTE: this rule will only be useful when validating multiple package.jsons at the same time
 */ export class AllVersionsTheSame extends ValidationRule { public readonly name = 'dependencies/versions-consistent' ; private readonly ourPackages : { [ pkg ] : string ; } = { } ; private readonly usedDeps : { [ pkg ] : VersionCount [ ] ; } = { } ; public prepare ( pkg ) { this . ourPackages [ pkg . json . name ] = pkg . json . version ; this . recordDeps ( pkg . json . dependencies ) ; this . recordDeps ( pkg . json . devDependencies ) ; } public validate ( pkg ) { this . validateDeps ( pkg , 'dependencies' ) ; this . validateDeps ( pkg , 'devDependencies' ) ; } private recordDeps ( deps ) { if ( ! deps ) { return ; } Object . keys ( deps ) . forEach ( dep => { this . recordDep ( dep , deps [ dep ] ) ; } ) ; } private validateDeps ( pkg , section ) { if ( ! pkg . json [ section ] ) { return ; } Object . keys ( pkg . json [ section ] ) . forEach ( dep => { this . validateDep ( pkg , section , dep ) ; } ) ; } private recordDep ( dep , version ) { if ( version === '*' ) { return ; } if ( ! ( dep in this . usedDeps ) ) { this . usedDeps [ dep ] = [ ] ; } const i = this . usedDeps [ dep ] . findIndex ( vc => vc . version === version ) ; if ( i === - 1 ) { this . usedDeps [ dep ] . push ( { version , count : 1 } ) ; } else { this . usedDeps [ dep ] [ i ] . count += 1 ; } } private validateDep ( pkg , depField , dep ) { if ( dep in this . ourPackages ) { expectJSON ( this . name , pkg , depField + '.' + dep , this . ourPackages [ dep ] ) ; return ; } if ( ! ( dep in this . usedDeps ) ) { return ; } const versions = this . usedDeps [ dep ] ; versions . sort ( ( a , b ) => b . count - a . count ) ; expectJSON ( this . name , pkg , depField + '.' + dep , versions [ 0 ] . version ) ; } } export class AwsLint extends ValidationRule { public readonly name = 'awslint' ; public validate ( pkg ) { if ( ! isJSII ( pkg ) ) { return ; } if ( ! isAWS ( pkg ) ) { return ; } expectJSON ( this . name , pkg , 'scripts.awslint' , 'cdk-awslint' ) ; } } export class Cfn2Ts extends ValidationRule { public readonly name = 'cfn2ts' ; public validate ( pkg ) { if ( ! isJSII ( pkg ) || ! isAWS ( pkg ) ) { return expectJSON ( this . name , pkg , 'scripts.cfn2ts' , undefined ) ; } expectJSON ( this . name , pkg , 'scripts.cfn2ts' , 'cfn2ts' ) ; } } /**
 * Packages inside JSII packages (typically used for embedding Lambda handles)
 * must only have dev dependencies and their node_modules must not be published.
 *
 * We might loosen this at some point but we'll have to bundle all runtime dependencies
 * and we don't have good transitive license checks.
 */ export class PackageInJsiiPackageNoRuntimeDeps extends ValidationRule { public readonly name = 'lambda-packages-no-runtime-deps' ; public validate ( pkg ) { if ( ! isJSII ( pkg ) ) { return ; } for ( const inner of findInnerPackages ( pkg . packageRoot ) ) { const innerPkg = PackageJson . fromDirectory ( inner ) ; if ( Object . keys ( innerPkg . dependencies ) . length > 0 ) { pkg . report ( { ruleName : `${ this . name }:1` , message : `NPM Package '${ innerPkg . packageName }' inside jsii package '${ pkg . packageName }', can only have devDependencies` , } ) ; } const nodeModulesRelPath = path . relative ( pkg . packageRoot , innerPkg . packageRoot ) + '/node_modules' ; fileShouldContain ( `${ this . name }:2` , pkg , '.npmignore' , nodeModulesRelPath ) ; } } } /**
 * Requires packages to have fast-fail build scripts, allowing to combine build, test and package/extract in a single command.
 * This involves multiple targets: `build+test`, `build+extract`, `build+test+extract`, and `build+test+package`
 */ export class FastFailingBuildScripts extends ValidationRule { public readonly name = 'fast-failing-build-scripts' ; public validate ( pkg ) { const scripts = pkg . json . scripts || { } ; const hasTest = 'test' in scripts ; const hasPack = 'package' in scripts ; const hasExtract = 'rosetta:extract' in scripts ; const cmdBuild = 'yarn build' ; expectJSON ( this . name , pkg , 'scripts.build+test' , hasTest ? [ cmdBuild , 'yarn test' ] . join ( ' && ' ) : cmdBuild ) ; expectJSON ( this . name , pkg , 'scripts.build+extract' , hasExtract ? [ cmdBuild , 'yarn rosetta:extract' ] . join ( ' && ' ) : cmdBuild ) ; const cmdBuildTest = 'yarn build+test' ; expectJSON ( this . name , pkg , 'scripts.build+test+package' , hasPack ? [ cmdBuildTest , 'yarn package' ] . join ( ' && ' ) : cmdBuildTest ) ; expectJSON ( this . name , pkg , 'scripts.build+test+extract' , hasExtract ? [ cmdBuildTest , 'yarn rosetta:extract' ] . join ( ' && ' ) : cmdBuildTest ) ; } } export class YarnNohoistBundledDependencies extends ValidationRule { public readonly name = 'yarn/nohoist-bundled-dependencies' ; public validate ( pkg ) { const bundled = pkg . json . bundleDependencies || pkg . json . bundledDependencies || [ ] ; if ( bundled . length === 0 ) { return ; } const repoPackageJson = path . resolve ( monoRepoRoot ( ) , 'package.json' ) ; const nohoist = require ( repoPackageJson ) . workspaces . nohoist ; const missing = new Array < string > ( ) ; for ( const dep of bundled ) { for ( const entry of [ `${ pkg . packageName }/${ dep }` , `${ pkg . packageName }/${ dep }/**` ] ) { if ( nohoist . indexOf ( entry ) >= 0 ) { continue ; } missing . push ( entry ) ; } } if ( missing . length > 0 ) { pkg . report ( { ruleName : this . name , message : `Repository-level 'workspaces.nohoist' directive is missing: ${ missing . join ( ', ' ) }` , fix : ( ) => { const packageJson = require ( repoPackageJson ) ; packageJson . workspaces . nohoist = [ ... packageJson . workspaces . nohoist , ... missing ] . sort ( ) ; fs . writeFileSync ( repoPackageJson , `${ JSON . stringify ( packageJson , null , 2 ) }\n` , { encoding : 'utf8' } ) ; } , } ) ; } } } export class ConstructsDependency extends ValidationRule { public readonly name = 'constructs/dependency' ; public validate ( pkg ) { const REQUIRED_VERSION = ConstructsVersion . VERSION ; ; if ( pkg . devDependencies ?. constructs && pkg . devDependencies ?. constructs !== REQUIRED_VERSION ) { pkg . report ( { ruleName : this . name , message : `"constructs" must have a version requirement ${ REQUIRED_VERSION }` , fix : ( ) => { pkg . addDevDependency ( 'constructs' , REQUIRED_VERSION ) ; } , } ) ; } if ( pkg . dependencies . constructs && pkg . dependencies . constructs !== REQUIRED_VERSION ) { pkg . report ( { ruleName : this . name , message : `"constructs" must have a version requirement ${ REQUIRED_VERSION }` , fix : ( ) => { pkg . addDependency ( 'constructs' , REQUIRED_VERSION ) ; } , } ) ; if ( ! pkg . peerDependencies . constructs || pkg . peerDependencies . constructs !== REQUIRED_VERSION ) { pkg . report ( { ruleName : this . name , message : `"constructs" must have a version requirement ${ REQUIRED_VERSION } in peerDependencies` , fix : ( ) => { pkg . addPeerDependency ( 'constructs' , REQUIRED_VERSION ) ; } , } ) ; } } } } /**
 * Packages must depend on 'assert-internal', not on '@aws-cdk/assert'
 */ export class AssertDependency extends ValidationRule { public readonly name = 'assert/assert-dependency' ; public validate ( pkg ) { const devDeps = pkg . json . devDependencies ?? { } ; if ( '@aws-cdk/assert' in devDeps ) { pkg . report ( { ruleName : this . name , message : 'Package should depend on \'@aws-cdk/assert-internal\', not on \'@aws-cdk/assert\'' , fix : ( ) => { pkg . json . devDependencies [ '@aws-cdk/assert-internal' ] = pkg . json . devDependencies [ '@aws-cdk/assert' ] ; delete pkg . json . devDependencies [ '@aws-cdk/assert' ] ; } , } ) ; } } } /**
 * Do not announce new versions of AWS CDK modules in awscdk.io because it is very very spammy
 * and actually causes the @awscdkio twitter account to be blocked.
 *
 * https://github.com/construct-catalog/catalog/issues/24
 * https://github.com/construct-catalog/catalog/pull/22
 */ export class DoNotAnnounceInCatalog extends ValidationRule { public readonly name = 'catalog/no-announce' ; public validate ( pkg ) { if ( ! isJSII ( pkg ) ) { return ; } if ( pkg . json . awscdkio ?. announce !== false ) { pkg . report ( { ruleName : this . name , message : 'missing "awscdkio.announce: false" in package.json' , fix : ( ) => { pkg . json . awscdkio = pkg . json . awscdkio ?? { } ; pkg . json . awscdkio . announce = false ; } , } ) ; } } } export class EslintSetup extends ValidationRule { public readonly name = 'package-info/eslint' ; public validate ( pkg ) { const eslintrcFilename = '.eslintrc.js' ; if ( ! fs . existsSync ( eslintrcFilename ) ) { pkg . report ( { ruleName : this . name , message : 'There must be a .eslintrc.js file at the root of the package' , fix : ( ) => { const rootRelative = path . relative ( pkg . packageRoot , repoRoot ( pkg . packageRoot ) ) ; fs . writeFileSync ( eslintrcFilename , [ `const baseConfig = require('${ rootRelative }/tools/@aws-cdk/cdk-build-tools/config/eslintrc');` , "baseConfig.parserOptions.project = __dirname + '/tsconfig.json';" , 'module.exports = baseConfig;' , ] . join ( '\n' ) + '\n' ) ; } , } ) ; } fileShouldContain ( this . name , pkg , '.gitignore' , '!.eslintrc.js' ) ; fileShouldContain ( this . name , pkg , '.npmignore' , '.eslintrc.js' ) ; } } export class JestSetup extends ValidationRule { public readonly name = 'package-info/jest.config' ; public validate ( pkg ) { const cdkBuild = pkg . json [ 'cdk-build' ] || { } ; if ( pkg . json . jest ) { pkg . report ( { ruleName : this . name , message : 'Using Jest is set through a flag in the "cdk-build" key in package.json, the "jest" key is ignored' , fix : ( ) => { delete pkg . json . jest ; cdkBuild . jest = true ; pkg . json [ 'cdk-build' ] = cdkBuild ; } , } ) ; } if ( ! cdkBuild . jest ) { return ; } const jestConfigFilename = 'jest.config.js' ; if ( ! fs . existsSync ( jestConfigFilename ) ) { pkg . report ( { ruleName : this . name , message : 'There must be a jest.config.js file at the root of the package' , fix : ( ) => { const rootRelative = path . relative ( pkg . packageRoot , repoRoot ( pkg . packageRoot ) ) ; fs . writeFileSync ( jestConfigFilename , [ `const baseConfig = require('${ rootRelative }/tools/@aws-cdk/cdk-build-tools/config/jest.config');` , 'module.exports = baseConfig;' , ] . join ( '\n' ) + '\n' ) ; } , } ) ; } fileShouldContain ( this . name , pkg , '.gitignore' , '!jest.config.js' ) ; fileShouldContain ( this . name , pkg , '.npmignore' , 'jest.config.js' ) ; if ( ! ( pkg . json . devDependencies ?? { } ) [ '@types/jest' ] ) { pkg . report ( { ruleName : `${ this . name }.types` , message : 'There must be a devDependency on \'@types/jest\' if you use jest testing' , } ) ; } } } export class UbergenPackageVisibility extends ValidationRule { public readonly name = 'ubergen/package-visibility' ; private readonly publicPackages = [ '@aws-cdk/cfnspec' , '@aws-cdk/cloud-assembly-schema' , '@aws-cdk/cloudformation-diff' , '@aws-cdk/cx-api' , '@aws-cdk/region-info' , 'aws-cdk-lib' , 'aws-cdk' , 'awslint' , 'cdk' , 'cdk-assets' , ] ; public validate ( pkg ) { if ( cdkMajorVersion ( ) === 2 ) { if ( this . publicPackages . includes ( pkg . json . name ) && pkg . json . private === true ) { pkg . report ( { ruleName : this . name , message : 'Package must be public' , fix : ( ) => { delete pkg . json . private ; } , } ) ; } else if ( ! this . publicPackages . includes ( pkg . json . name ) && pkg . json . private !== true && ! pkg . packageName . endsWith ( '-alpha' ) ) { pkg . report ( { ruleName : this . name , message : 'Package must not be public' , fix : ( ) => { delete pkg . json . private ; pkg . json . private = true ; } , } ) ; } } else { if ( pkg . json . private && ! pkg . json . ubergen ?. exclude ) { pkg . report ( { ruleName : this . name , message : 'ubergen.exclude must be configured for private packages' , fix : ( ) => { pkg . json . ubergen = { exclude : true , } ; } , } ) ; } } } } /**
 * No experimental dependencies.
 * In v2 all experimental modules will be released separately from aws-cdk-lib. This means that:
 * 1. Stable modules can't depend on experimental modules as it will creates a cyclic dependency.
 * 2. Experimental modules shouldn't depend on experimental modules as it will create a coupling between their graduation (cause of 1).
 * 2 specify "shouldn't" as in some cases we might allow it (using the `excludedDependencies` map), but the default is to not allow it.
 */ export class NoExperimentalDependents extends ValidationRule { public name = 'no-experimental-dependencies' ; private readonly excludedDependencies = new Map ( [ [ '@aws-cdk/aws-secretsmanager' , [ '@aws-cdk/aws-sam' ] ] , [ '@aws-cdk/aws-kinesisanalytics-flink' , [ '@aws-cdk/aws-kinesisanalytics' ] ] , [ '@aws-cdk/aws-apigatewayv2-integrations' , [ '@aws-cdk/aws-apigatewayv2' ] ] , [ '@aws-cdk/aws-apigatewayv2-authorizers' , [ '@aws-cdk/aws-apigatewayv2' ] ] , [ '@aws-cdk/aws-events-targets' , [ '@aws-cdk/aws-kinesisfirehose' ] ] , [ '@aws-cdk/aws-kinesisfirehose-destinations' , [ '@aws-cdk/aws-kinesisfirehose' ] ] , [ '@aws-cdk/aws-iot-actions' , [ '@aws-cdk/aws-iot' , '@aws-cdk/aws-kinesisfirehose' ] ] , ] ) ; private readonly excludedModules = [ '@aws-cdk/cloudformation-include' ] ; public validate ( pkg ) { if ( this . excludedModules . includes ( pkg . packageName ) ) { return ; } if ( ! isCdkModuleName ( pkg . packageName ) ) { return ; } if ( ! isIncludedInMonolith ( pkg ) ) { return ; } Object . keys ( pkg . dependencies ) . forEach ( dep => { if ( ! isCdkModuleName ( dep ) ) { return ; } const stability = require ( `${ dep }/package.json` ) . stability ; if ( stability === 'experimental' ) { if ( this . excludedDependencies . get ( pkg . packageName ) ?. includes ( dep ) ) { return ; } pkg . report ( { ruleName : this . name , message : `It is not allowed to depend on experimental modules. ${ pkg . packageName } added a dependency on experimental module ${ dep }` , } ) ; } } ) ; } } export class AwsCdkLibReadmeMatchesCore extends ValidationRule { public readonly name = 'package-info/README.md/aws-cdk-lib-and-core' ; private readonly CORE_DOC_SECTION_REGEX = /<\!--BEGIN CORE DOCUMENTATION-->[\s\S]+<\!--END CORE DOCUMENTATION-->/m ; public validate ( pkg ) { if ( pkg . json . name !== 'aws-cdk-lib' ) { return ; } const coreReadmeFile = path . join ( monoRepoRoot ( ) , 'packages' , '@aws-cdk' , 'core' , 'README.md' ) ; const readmeFile = path . join ( pkg . packageRoot , 'README.md' ) ; const awsCoreMatch = fs . readFileSync ( coreReadmeFile , { encoding : 'utf8' } ) . match ( this . CORE_DOC_SECTION_REGEX ) ; const awsCdkLibReadme = fs . readFileSync ( readmeFile , { encoding : 'utf8' } ) ; const awsCdkLibMatch = awsCdkLibReadme . match ( this . CORE_DOC_SECTION_REGEX ) ; const missingSectionMsg = '@aws-cdk/core and aws-cdk-lib READMEs must include section markers (<!--BEGIN/END CORE DOCUMENTATION-->) to define what content should be shared between them' ; if ( ! awsCoreMatch ) { pkg . report ( { ruleName : this . name , message : missingSectionMsg , } ) ; } else if ( ! awsCdkLibMatch ) { pkg . report ( { ruleName : this . name , message : missingSectionMsg , fix : ( ) => fs . writeFileSync ( readmeFile , [ awsCdkLibReadme , awsCoreMatch [ 0 ] ] . join ( '\n' ) ) , } ) ; } else if ( awsCoreMatch [ 0 ] !== awsCdkLibMatch [ 0 ] ) { pkg . report ( { ruleName : this . name , message : 'aws-cdk-lib README does not include a core documentation section that matches @aws-cdk/core' , fix : ( ) => fs . writeFileSync ( readmeFile , awsCdkLibMatch . input ! . replace ( this . CORE_DOC_SECTION_REGEX , awsCoreMatch [ 0 ] ) ) , } ) ; } } } /**
 * Enforces that the aws-cdk's package.json on the V2 branch does not have the "main"
 * and "types" keys filled.
 */ export class CdkCliV2MissesMainAndTypes extends ValidationRule { public readonly name = 'aws-cdk/cli/v2/package.json/main' ; public validate ( pkg ) { if ( pkg . json . name !== 'aws-cdk' ) { return ; } if ( cdkMajorVersion ( ) === 1 ) { return ; } if ( pkg . json . main || pkg . json . types ) { pkg . report ( { ruleName : this . name , message : 'The package.json file for the aws-cdk CLI package in V2 cannot have "main" and "types" keys' , fix : ( ) => { delete pkg . json . main ; delete pkg . json . types ; } , } ) ; } } } /**
 * Determine whether this is a JSII package
 *
 * A package is a JSII package if there is 'jsii' section in the package.json
 */ function isJSII ( pkg ) { return ( pkg . json . jsii !== undefined ) ; } param pkg function isAWS ( pkg ) { return pkg . json [ 'cdk-build' ] ?. cloudformation != null ; } /**
 * Determine whether the package has tests
 *
 * A package has tests if the root/test directory exists
 */ function hasTestDirectory ( pkg ) { return fs . existsSync ( path . join ( pkg . packageRoot , 'test' ) ) ; } /**
 * Whether this package has integ tests
 *
 * A package has integ tests if it mentions 'cdk-integ' in the "test" script.
 */ function hasIntegTests ( pkg ) { if ( ! hasTestDirectory ( pkg ) ) { return false ; } const files = fs . readdirSync ( path . join ( pkg . packageRoot , 'test' ) ) ; return files . some ( p => p . startsWith ( 'integ.' ) ) ; } /**
 * Return whether this package should use CDK build tools
 */ function shouldUseCDKBuildTools ( pkg ) { const exclude = [ '@aws-cdk/cdk-build-tools' , '@aws-cdk/script-tests' , 'awslint' , ] ; return ! exclude . includes ( pkg . packageName ) ; } function repoRoot ( dir ) { let root = dir ; for ( let i = 0 ; i < 50 && ! fs . existsSync ( path . join ( root , 'yarn.lock' ) ) ; i ++ ) { root = path . dirname ( root ) ; } return root ; } function toRegExp ( str ) { return new RegExp ( str . replace ( /[.*+?^${}()|[\]\\]/g , '\\$&' ) . replace ( /\w+/g , '\\w+' ) ) ; } function readBannerFile ( file ) { return fs . readFileSync ( path . join ( __dirname , 'banners' , file ) , { encoding : 'utf-8' } ) . trim ( ) ; } function cdkMajorVersion ( ) { const releaseJson = require ( `${ monoRepoRoot ( ) }/release.json` ) ; return releaseJson . majorVersion as number ; } /**
 * Should this package be included in the monolithic package.
 */ function isIncludedInMonolith ( pkg ) { if ( pkg . json . ubergen ?. exclude ) { return false ; } else if ( ! isJSII ( pkg ) ) { return false ; } else if ( pkg . json . deprecated ) { return false ; } return true ; } function beginEndRegex ( label ) { return new RegExp ( `(<\!--BEGIN ${ label }-->)([\s\S]+)(<\!--END ${ label }-->)` , 'm' ) ; } function readIfExists ( filename ) { return fs . existsSync ( filename ) ? fs . readFileSync ( filename , { encoding : 'utf8' } ) : undefined ; }