Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@
"chalk": "^4.1.2",
"ci-info": "^4.3.1",
"conf": "^10.2.0",
"debug": "^4.4.3",
"dotenv": "^16.5.0",
"execa": "^9.6.1",
"git-repo-info": "^2.1.1",
Expand All @@ -115,6 +116,7 @@
"@playwright/test": "^1.57.0",
"@types/archiver": "6.0.3",
"@types/config": "^3.3.5",
"@types/debug": "^4.1.12",
"@types/glob": "^8.1.0",
"@types/luxon": "^3.7.1",
"@types/node": "^22.14.1",
Expand Down
15 changes: 9 additions & 6 deletions packages/cli/src/commands/baseCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import prompts from 'prompts'
import { Command } from '@oclif/core'
import { api } from '../rest/api'
import { CommandStyle } from '../helpers/command-style'
import { PackageFilesResolver } from '../services/check-parser/package-files/resolver'
import { PackageJsonFile } from '../services/check-parser/package-files/package-json-file'
import { detectNearestPackageJson } from '../services/check-parser/package-files/package-manager'

export type BaseCommandClass = typeof Command & {
coreCommand: boolean
Expand All @@ -18,12 +18,15 @@ export abstract class BaseCommand extends Command {
#packageJsonLoader?: Promise<PackageJsonFile | undefined>

async loadPackageJsonOfSelf (): Promise<PackageJsonFile | undefined> {
if (!this.#packageJsonLoader) {
const resolver = new PackageFilesResolver()
this.#packageJsonLoader = resolver.loadPackageJsonFile(__filename)
}
try {
if (!this.#packageJsonLoader) {
this.#packageJsonLoader = detectNearestPackageJson(__dirname)
}

return await this.#packageJsonLoader
return await this.#packageJsonLoader
} catch {
return
}
}

async checkEngineCompatibility (): Promise<void> {
Expand Down
12 changes: 6 additions & 6 deletions packages/cli/src/commands/import/plan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,8 @@ import { ExitError } from '@oclif/core/errors'
import { confirmCommit, performCommitAction } from './commit'
import { confirmApply, performApplyAction } from './apply'
import { generateChecklyConfig } from '../../services/checkly-config-codegen'
import { PackageFilesResolver } from '../../services/check-parser/package-files/resolver'
import { PackageJsonFile } from '../../services/check-parser/package-files/package-json-file'
import { detectPackageManager, knownPackageManagers, PackageManager } from '../../services/check-parser/package-files/package-manager'
import { detectNearestPackageJson, detectPackageManager, knownPackageManagers, PackageManager } from '../../services/check-parser/package-files/package-manager'
import { parseProject } from '../../services/project-parser'
import { Runtime } from '../../rest/runtimes'
import { ConstructExport, Project, Session } from '../../constructs/project'
Expand Down Expand Up @@ -704,10 +703,11 @@ ${chalk.cyan('For safety, resources are not deletable until the plan has been co
}

async #loadPackageJson (): Promise<PackageJsonFile | undefined> {
const resolver = new PackageFilesResolver()
return await resolver.loadPackageJsonFile(process.cwd(), {
isDir: true,
})
try {
return await detectNearestPackageJson(process.cwd())
} catch {
return
}
}

#createPackageJson (logicalId: string): PackageJsonFile {
Expand Down
7 changes: 3 additions & 4 deletions packages/cli/src/constructs/browser-check.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
import fs from 'node:fs/promises'
import path from 'node:path'

import { CheckProps, RuntimeCheck, RuntimeCheckProps } from './check'
import { Session, SharedFileRef } from './project'
import { pathToPosix } from '../services/util'
import { Content, Entrypoint, isContent, isEntrypoint } from './construct'
import { detectSnapshots } from '../services/snapshot-service'
import { PlaywrightConfig } from './playwright-config'
import { Diagnostics } from './diagnostics'
import { InvalidPropertyValueDiagnostic } from './construct-diagnostics'
import { BrowserCheckBundle } from './browser-check-bundle'
import { ConfigDefaultsGetter, makeConfigDefaultsGetter } from './check-config'
import { CheckConfigDefaults } from '../services/checkly-config-loader'

export interface BrowserCheckProps extends RuntimeCheckProps {
/**
Expand Down Expand Up @@ -106,7 +105,7 @@ export class BrowserCheck extends RuntimeCheck {
return `BrowserCheck:${this.logicalId}`
}

protected configDefaultsGetter (props: CheckProps): ConfigDefaultsGetter {
protected configDefaultsGetter (props: CheckProps): ConfigDefaultsGetter<CheckConfigDefaults> {
return makeConfigDefaultsGetter(
props.group?.getBrowserCheckDefaults(),
Session.browserCheckDefaults,
Expand Down Expand Up @@ -168,7 +167,7 @@ export class BrowserCheck extends RuntimeCheck {
const deps: SharedFileRef[] = []
for (const { filePath, content } of parsed.dependencies) {
deps.push(Session.registerSharedFile({
path: pathToPosix(path.relative(Session.basePath!, filePath)),
path: Session.relativePosixPath(filePath),
content,
}))
}
Expand Down
14 changes: 7 additions & 7 deletions packages/cli/src/constructs/check-config.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { CheckConfigDefaults } from '../services/checkly-config-loader'
export type ConfigDefaultsGetter<
T extends object,
> = <K extends keyof T = keyof T>(key: K) => T[K] | undefined

export type ConfigDefaultsGetter = <K extends keyof CheckConfigDefaults> (key: K) => CheckConfigDefaults[K]

export function makeConfigDefaultsGetter (
...defaults: (Partial<CheckConfigDefaults> | undefined)[]
): ConfigDefaultsGetter {
export function makeConfigDefaultsGetter<T extends object> (
...defaults: (Partial<T> | undefined)[]
): ConfigDefaultsGetter<T> {
const ok = defaults.filter(value => value !== undefined)

function get<K extends keyof CheckConfigDefaults> (key: K): CheckConfigDefaults[K] {
function get<K extends keyof T> (key: K): T[K] | undefined {
for (const config of ok) {
// Older TS seems to need this check.
if (config === undefined) {
Expand Down
3 changes: 1 addition & 2 deletions packages/cli/src/constructs/check-group-v1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import { Diagnostics } from './diagnostics'
import { DeprecatedConstructDiagnostic, DeprecatedPropertyDiagnostic, InvalidPropertyValueDiagnostic } from './construct-diagnostics'
import CheckTypes from '../constants'
import { CheckConfigDefaults } from '../services/checkly-config-loader'
import { pathToPosix } from '../services/util'
import { AlertChannelSubscription } from './alert-channel-subscription'
import { BrowserCheck } from './browser-check'
import { CheckGroupRef } from './check-group-ref'
Expand Down Expand Up @@ -438,7 +437,7 @@ export class CheckGroupV1 extends Construct {
},
// the browserChecks props inherited from the group are applied in BrowserCheck.constructor()
}
const checkLogicalId = pathToPosix(path.relative(Session.basePath!, filepath))
const checkLogicalId = Session.relativePosixPath(filepath)
if (checkType === CheckTypes.BROWSER) {
new BrowserCheck(checkLogicalId, props)
} else {
Expand Down
3 changes: 2 additions & 1 deletion packages/cli/src/constructs/check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { ConfigDefaultsGetter, makeConfigDefaultsGetter } from './check-config'
import { Diagnostics } from './diagnostics'
import { validateDeprecatedDoubleCheck } from './internal/common-diagnostics'
import { InvalidPropertyValueDiagnostic } from './construct-diagnostics'
import { CheckConfigDefaults } from '../services/checkly-config-loader'

/**
* Retry strategies supported by checks.
Expand Down Expand Up @@ -329,7 +330,7 @@ export abstract class Check extends Construct {
await this.validateRetryStrategyOnlyOn(diagnostics)
}

protected configDefaultsGetter (props: CheckProps): ConfigDefaultsGetter {
protected configDefaultsGetter (props: CheckProps): ConfigDefaultsGetter<CheckConfigDefaults> {
return makeConfigDefaultsGetter(
props.group?.getCheckDefaults(),
Session.checkDefaults,
Expand Down
14 changes: 14 additions & 0 deletions packages/cli/src/constructs/construct-diagnostics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,20 @@ export class UnsupportedRuntimeFeatureDiagnostic extends ErrorDiagnostic {
}
}

export class UnsatisfiedLocalPrerequisitesDiagnostic extends ErrorDiagnostic {
constructor (error: Error) {
super({
title: `Unsatisfied local prerequisites`,
message:
`Local environment does not satisfy the prerequisites for this `
+ `functionality.`
+ `\n\n`
+ `Cause: ${error.message}`,
error,
})
}
}

export class ConstructDiagnostic extends Diagnostic {
underlying: Diagnostic

Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/constructs/internal/codegen/snippet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,6 @@ export function parseSnippetDependencies (content: string): string[] {
}

return dependencies
.filter(value => value.startsWith(SNIPPET_PATH_PREFIX))
.map(value => value.slice(SNIPPET_PATH_PREFIX.length))
.filter(({ importPath }) => importPath.startsWith(SNIPPET_PATH_PREFIX))
.map(({ importPath }) => importPath.slice(SNIPPET_PATH_PREFIX.length))
}
3 changes: 2 additions & 1 deletion packages/cli/src/constructs/multi-step-check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { Diagnostics } from './diagnostics'
import { InvalidPropertyValueDiagnostic, UnsupportedRuntimeFeatureDiagnostic } from './construct-diagnostics'
import { MultiStepCheckBundle } from './multi-step-check-bundle'
import { ConfigDefaultsGetter, makeConfigDefaultsGetter } from './check-config'
import { CheckConfigDefaults } from '../services/checkly-config-loader'

export interface MultiStepCheckProps extends RuntimeCheckProps {
/**
Expand Down Expand Up @@ -96,7 +97,7 @@ export class MultiStepCheck extends RuntimeCheck {
}
}

protected configDefaultsGetter (props: CheckProps): ConfigDefaultsGetter {
protected configDefaultsGetter (props: CheckProps): ConfigDefaultsGetter<CheckConfigDefaults> {
return makeConfigDefaultsGetter(
props.group?.getMultiStepCheckDefaults(),
Session.multiStepCheckDefaults,
Expand Down
59 changes: 52 additions & 7 deletions packages/cli/src/constructs/playwright-check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { createReadStream } from 'node:fs'
import fs from 'node:fs/promises'

import type { AxiosResponse } from 'axios'
import Debug from 'debug'

import { checklyStorage } from '../rest/api'
import {
bundlePlayWrightProject, cleanup,
Expand All @@ -11,13 +13,17 @@ import {
ConflictingPropertyDiagnostic,
DeprecatedPropertyDiagnostic,
InvalidPropertyValueDiagnostic,
UnsatisfiedLocalPrerequisitesDiagnostic,
UnsupportedPropertyDiagnostic,
} from './construct-diagnostics'
import { Diagnostics } from './diagnostics'
import { PlaywrightCheckBundle } from './playwright-check-bundle'
import { Session } from './project'
import { Ref } from './ref'
import { ConfigDefaultsGetter, makeConfigDefaultsGetter } from './check-config'
import { CheckConfigDefaults } from '../services/checkly-config-loader'

const debug = Debug('checkly:cli:constructs:playwright-check')

export interface PlaywrightCheckProps extends Omit<RuntimeCheckProps, 'retryStrategy' | 'doubleCheck'> {
/**
Expand Down Expand Up @@ -125,7 +131,7 @@ export interface PlaywrightCheckProps extends Omit<RuntimeCheckProps, 'retryStra
*/
export class PlaywrightCheck extends RuntimeCheck {
installCommand?: string
testCommand: string
testCommand?: string
playwrightConfigPath: string
pwProjects: string[]
pwTags: string[]
Expand All @@ -148,7 +154,7 @@ export class PlaywrightCheck extends RuntimeCheck {
this.include = config.include
? (Array.isArray(config.include) ? config.include : [config.include])
: []
this.testCommand = config.testCommand ?? 'npx playwright test'
this.testCommand = config.testCommand
this.groupName = config.groupName
this.playwrightConfigPath = this.resolveContentFilePath(config.playwrightConfigPath)
Session.registerConstruct(this)
Expand All @@ -160,7 +166,7 @@ export class PlaywrightCheck extends RuntimeCheck {
return `PlaywrightCheck:${this.logicalId}`
}

protected configDefaultsGetter (props: PlaywrightCheckProps): ConfigDefaultsGetter {
protected configDefaultsGetter (props: PlaywrightCheckProps): ConfigDefaultsGetter<CheckConfigDefaults> {
const group = PlaywrightCheck.#resolveGroupFromProps(props)

return makeConfigDefaultsGetter(
Expand Down Expand Up @@ -239,6 +245,7 @@ export class PlaywrightCheck extends RuntimeCheck {

async validate (diagnostics: Diagnostics): Promise<void> {
await super.validate(diagnostics)
await this.#validateWorkspace(diagnostics)
await this.validateRetryStrategy(diagnostics)

try {
Expand All @@ -253,6 +260,29 @@ export class PlaywrightCheck extends RuntimeCheck {
this.#validateGroupReferences(diagnostics)
}

// eslint-disable-next-line require-await
async #validateWorkspace (diagnostics: Diagnostics): Promise<void> {
const workspace = Session.workspace
if (workspace.isOk()) {
const lockfile = workspace.ok().lockfile
if (lockfile.isErr()) {
diagnostics.add(new UnsatisfiedLocalPrerequisitesDiagnostic(new Error(
`A lockfile is required for Playwright checks, but none could be `
+ `detected.`
+ '\n\n'
+ `Cause: ${lockfile.err().message}`,
)))
}
} else if (workspace.isErr()) {
diagnostics.add(new UnsatisfiedLocalPrerequisitesDiagnostic(new Error(
`A workspace is required for Playwright checks, but none could be `
+ `detected.`
+ '\n\n'
+ `Cause: ${workspace.err().message}`,
)))
}
}

#validateGroupReferences (diagnostics: Diagnostics): void {
if (this.groupName) {
diagnostics.add(new DeprecatedPropertyDiagnostic(
Expand Down Expand Up @@ -294,6 +324,12 @@ export class PlaywrightCheck extends RuntimeCheck {
return `${testCommand} --config ${quotedPath}${projectArg}${tagArg}`
}

static contextifyCommand (command: string): string {
return Session.basePath === Session.contextPath
? command
: `env --chdir "${Session.relativePosixPath(Session.contextPath!)}" -- ${command}`
}

static async bundleProject (playwrightConfigPath: string, include: string[]) {
let dir = ''
try {
Expand All @@ -304,7 +340,12 @@ export class PlaywrightCheck extends RuntimeCheck {
const { data: { key } } = await PlaywrightCheck.uploadPlaywrightProject(dir)
return { key, browsers, relativePlaywrightConfigPath, cacheHash, playwrightVersion }
} finally {
await cleanup(dir)
if (process.env['CHECKLY_PLAYWRIGHT_DEBUG_PERSIST_BUNDLE'] === '1') {
debug(`Skip cleaning up bundle '${dir}'`)
} else {
debug(`Cleaning up bundle '${dir}'`)
await cleanup(dir)
}
}
}

Expand Down Expand Up @@ -332,12 +373,12 @@ export class PlaywrightCheck extends RuntimeCheck {
relativePlaywrightConfigPath,
} = await PlaywrightCheck.bundleProject(this.playwrightConfigPath, this.include ?? [])

const testCommand = PlaywrightCheck.buildTestCommand(
this.testCommand,
const testCommand = PlaywrightCheck.contextifyCommand(PlaywrightCheck.buildTestCommand(
this.testCommand ?? this.#defaultTestCommand(),
relativePlaywrightConfigPath,
this.pwProjects,
this.pwTags,
)
))

return new PlaywrightCheckBundle(this, {
groupId,
Expand All @@ -350,6 +391,10 @@ export class PlaywrightCheck extends RuntimeCheck {
})
}

#defaultTestCommand (): string {
return Session.packageManager.execCommand(['playwright', 'test']).unsafeDisplayCommand
}

synthesize () {
return {
...super.synthesize(),
Expand Down
Loading
Loading