Skip to content

Ferran/sc 23256/pwt native code package #1036

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
1,027 changes: 984 additions & 43 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@
"@typescript-eslint/typescript-estree": "8.24.1",
"acorn": "8.14.0",
"acorn-walk": "8.3.4",
"@types/archiver": "6.0.3",
"archiver": "7.0.1",
"axios": "1.7.4",
"chalk": "4.1.2",
"ci-info": "3.8.0",
Expand Down
10 changes: 6 additions & 4 deletions packages/cli/src/commands/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,10 @@ export default class Deploy extends AuthCommand {
default: false,
hidden: true,
}),
'debug-bundle-output-file': Flags.string({
description: 'The file to output the debug debug bundle to.',
default: './debug-bundle.json',
hidden: true,
playwrightConfig: Flags.string({
char: 'p',
description: 'File path to playwright config file',
allowNo: true,
}),
}

Expand All @@ -88,6 +88,7 @@ export default class Deploy extends AuthCommand {
'verify-runtime-dependencies': verifyRuntimeDependencies,
'debug-bundle': debugBundle,
'debug-bundle-output-file': debugBundleOutputFile,
playwrightConfig,
} = flags
const { configDirectory, configFilenames } = splitConfigFilePath(configFilename)
const {
Expand All @@ -114,6 +115,7 @@ export default class Deploy extends AuthCommand {
defaultRuntimeId: account.runtimeId,
verifyRuntimeDependencies,
checklyConfigConstructs,
playwrightConfig,
})
const repoInfo = getGitInformation(project.repoUrl)

Expand Down
7 changes: 7 additions & 0 deletions packages/cli/src/commands/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,11 @@ export default class Test extends AuthCommand {
allowNo: true,
env: 'CHECKLY_VERIFY_RUNTIME_DEPENDENCIES',
}),
playwrightConfig: Flags.string({
char: 'p',
description: 'File path to playwright config file',
allowNo: true,
}),
}

static args = {
Expand Down Expand Up @@ -146,6 +151,7 @@ export default class Test extends AuthCommand {
'update-snapshots': updateSnapshots,
retries,
'verify-runtime-dependencies': verifyRuntimeDependencies,
playwrightConfig,
} = flags
const filePatterns = argv as string[]

Expand Down Expand Up @@ -182,6 +188,7 @@ export default class Test extends AuthCommand {
defaultRuntimeId: account.runtimeId,
verifyRuntimeDependencies,
checklyConfigConstructs,
playwrightConfig,
})
const checks = Object.entries(project.data.check)
.filter(([, check]) => {
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/constructs/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,4 @@ export * from './tcp-check'
export * from './incidentio-alert-channel'
export * from './msteams-alert-channel'
export * from './telegram-alert-channel'
export * from './playwright-check'
43 changes: 43 additions & 0 deletions packages/cli/src/constructs/playwright-check.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { Check, CheckProps } from './check'
import { Session } from './project'
import {
bundlePlayWrightProject, cleanup,
uploadPlaywrightProject,
} from '../services/util'

export interface PlaywrightCheckProps extends CheckProps {
codeBundlePath: string
}

export class PlaywrightCheck extends Check {
private codeBundlePath: string
constructor (logicalId: string, props: PlaywrightCheckProps) {
super(logicalId, props)
this.codeBundlePath = props.codeBundlePath
Session.registerConstruct(this)
}

getSourceFile () {
return this.__checkFilePath ?? this.logicalId
}

static async bundleProject (playwrightConfigPath: string) {
let dir = ''
try {
dir = await bundlePlayWrightProject(playwrightConfigPath)
const { data: { key } } = await uploadPlaywrightProject(dir)
return key
} catch (e: Error | any) {
} finally {
cleanup(dir)
}
}

synthesize () {
return {
...super.synthesize(),
checkType: 'PLAYWRIGHT',
codeBundlePath: this.codeBundlePath,
}
}
}
8 changes: 8 additions & 0 deletions packages/cli/src/rest/checkly-storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@ class ChecklyStorage {
)
}

uploadCodeBundle (stream: Readable, size: number) {
return this.api.post<{ key: string }>(
'/next/checkly-storage/upload-code-bundle',
stream,
{ headers: { 'Content-Type': 'application/octet-stream', 'content-length': size } },
)
}

download (key: string) {
return this.api.post('/next/checkly-storage/download', { key }, { responseType: 'stream' })
}
Expand Down
55 changes: 34 additions & 21 deletions packages/cli/src/services/project-parser.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
import { glob } from 'glob'
import * as path from 'path'
import { loadJsFile, loadTsFile, pathToPosix } from './util'
import {
bundlePlayWrightProject, cleanup,
findFilesWithPattern,
loadJsFile,
loadTsFile,
pathToPosix, uploadPlaywrightProject,
} from './util'

import {
Check, BrowserCheck, CheckGroup, Project, Session,
PrivateLocation, PrivateLocationCheckAssignment, PrivateLocationGroupAssignment, MultiStepCheck,
} from '../constructs'
import { Ref } from '../constructs/ref'
import { CheckConfigDefaults } from './checkly-config-loader'

import type { Runtime } from '../rest/runtimes'
import type { Construct } from '../constructs/construct'
import { PlaywrightCheck } from '../constructs/playwright-check'

type ProjectParseOpts = {
directory: string,
Expand All @@ -26,6 +32,7 @@ type ProjectParseOpts = {
defaultRuntimeId: string,
verifyRuntimeDependencies?: boolean,
checklyConfigConstructs?: Construct[],
playwrightConfig?: string
}

const BASE_CHECK_DEFAULTS = {
Expand All @@ -47,6 +54,7 @@ export async function parseProject (opts: ProjectParseOpts): Promise<Project> {
defaultRuntimeId,
verifyRuntimeDependencies,
checklyConfigConstructs,
playwrightConfig,
} = opts
const project = new Project(projectLogicalId, {
name: projectName,
Expand All @@ -65,15 +73,35 @@ export async function parseProject (opts: ProjectParseOpts): Promise<Project> {

// TODO: Do we really need all of the ** globs, or could we just put node_modules?
const ignoreDirectories = ['**/node_modules/**', '**/.git/**', ...ignoreDirectoriesMatch]
await loadAllCheckFiles(directory, checkMatch, ignoreDirectories)
await loadAllBrowserChecks(directory, browserCheckMatch, ignoreDirectories, project)
await loadAllMultiStepChecks(directory, multiStepCheckMatch, ignoreDirectories, project)
await Promise.all([
loadAllCheckFiles(directory, checkMatch, ignoreDirectories),
loadAllBrowserChecks(directory, browserCheckMatch, ignoreDirectories, project),
loadAllMultiStepChecks(directory, multiStepCheckMatch, ignoreDirectories, project),
loadPlaywrightProject(playwrightConfig),
])

// private-location must be processed after all checks and groups are loaded.
await loadAllPrivateLocationsSlugNames(project)

return project
}
async function loadPlaywrightProject (playwrightConfig: string | undefined) {
if (!playwrightConfig) {
return
}
let dir = ''
try {
dir = await bundlePlayWrightProject(playwrightConfig)
const { data: { key } } = await uploadPlaywrightProject(dir)
const playwrightCheck = new PlaywrightCheck(playwrightConfig, {
name: path.basename(playwrightConfig),
codeBundlePath: key,
})
} catch (e: Error | any) {
} finally {
cleanup(dir)
}
}

async function loadAllCheckFiles (
directory: string,
Expand Down Expand Up @@ -223,18 +251,3 @@ async function loadAllPrivateLocationsSlugNames (
})
})
}

async function findFilesWithPattern (
directory: string,
pattern: string | string[],
ignorePattern: string[],
): Promise<string[]> {
// The files are sorted to make sure that the processing order is deterministic.
const files = await glob(pattern, {
nodir: true,
cwd: directory,
ignore: ignorePattern,
absolute: true,
})
return files.sort()
}
144 changes: 143 additions & 1 deletion packages/cli/src/services/util.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { CreateAxiosDefaults } from 'axios'
import type { AxiosResponse, CreateAxiosDefaults } from 'axios'
import * as path from 'path'
import * as fs from 'fs/promises'
import * as fsSync from 'fs'
Expand All @@ -8,6 +8,11 @@ import { parse } from 'dotenv'
// @ts-ignore
import { getProxyForUrl } from 'proxy-from-env'
import { httpOverHttp, httpsOverHttp, httpOverHttps, httpsOverHttps } from 'tunnel'
import { Archiver, create } from 'archiver'
import { glob } from 'glob'
import { Parser } from './check-parser/parser'
import { checklyStorage } from '../rest/api'
import { loadFile } from './checkly-config-loader'

// Copied from oclif/core
// eslint-disable-next-line
Expand Down Expand Up @@ -223,3 +228,140 @@ export function assignProxy (baseURL: string, axiosConfig: CreateAxiosDefaults)
axiosConfig.proxy = false
return axiosConfig
}

export async function bundlePlayWrightProject (playwrightConfig: string): Promise<string> {
const dir = path.resolve(path.dirname(playwrightConfig))
const filePath = path.resolve(dir, playwrightConfig)
const pwtFileName = path.basename(filePath)
const pwtConfig = await loadFile(filePath)
const outputDir = path.join(dir, 'playwright-project.tar.gz')
const output = fsSync.createWriteStream(outputDir)

const archive = create('tar', {
gzip: true,
gzipOptions: {
level: 9,
},
})
archive.pipe(output)
archive.append(fsSync.createReadStream(filePath), { name: pwtFileName })
const { packageJson, packageLock } = getPackageJsonFiles(dir)
archive.append(fsSync.createReadStream(packageJson), { name: 'package.json' })
archive.append(fsSync.createReadStream(packageLock), { name: 'package-lock.json' })
const files = await loadPlaywrightProjectFiles(dir, pwtConfig, archive)
loadFilesDependencies(dir, files, archive)
await archive.finalize()
return new Promise((resolve, reject) => {
output.on('close', () => {
return resolve(outputDir)
})

output.on('error', (err) => {
return reject(err)
})
})
}

export function getPackageJsonFiles (dir: string) {
const packageJson = path.resolve(dir, 'package.json')
const packageLock = path.resolve(dir, 'package-lock.json')
return { packageJson, packageLock }
}

export async function loadPlaywrightProjectFiles (dir: string, playWrightConfig: any, archive: Archiver) {
const files: string[] = []
if (playWrightConfig.testDir) {
archive.directory(path.resolve(dir, playWrightConfig.testDir), path.basename(playWrightConfig.testDir))
files.push(...getFiles(path.resolve(dir, playWrightConfig.testDir)))
}
if (playWrightConfig.testMatch) {
if (Array.isArray(playWrightConfig.testMatch)) {
const arr = await Promise
.all(playWrightConfig.testMatch
.map((pattern: string) => loadPatternWithDependencies(pattern, dir, archive)))
files.push(...arr.flatMap(x => x))
} else {
const arr = await loadPatternWithDependencies(playWrightConfig.testMatch, dir, archive)
files.push(...arr)
}
}
for (const project of playWrightConfig.projects) {
if (project.testDir) {
archive.directory(path.resolve(dir, project.testDir), project.testDir)
files.push(...getFiles(path.resolve(dir, project.testDir)))
}
if (project.testMatch) {
if (Array.isArray(project.testMatch)) {
const arr = await Promise
.all(project.testMatch
.map((pattern: string) => loadPatternWithDependencies(pattern, dir, archive)))
files.push(...arr.flatMap(x => x))
} else {
const arr = await loadPatternWithDependencies(project.testMatch, dir, archive)
files.push(...arr)
}
}
}
return files
}

function loadPatternWithDependencies (pattern: string, dir: string, archive: Archiver) {
archive.glob(pattern, { cwd: dir })
return findFilesWithPattern(dir, pattern, [])
}

export function loadFilesDependencies (dir: string, files: string[], archive: Archiver) {
const parser = new Parser({
checkUnsupportedModules: false,
})
const dependencyFiles = files
.map(file => parser.parse(file))
.flatMap(({ dependencies }) => dependencies)
.map(({ filePath }) => filePath)
new Set(dependencyFiles)
.forEach(dep => {
const relPath = dep.replace(dir, '')
archive.append(fsSync.createReadStream(dep), { name: relPath })
})
}

export async function findFilesWithPattern (
directory: string,
pattern: string | string[],
ignorePattern: string[],
): Promise<string[]> {
// The files are sorted to make sure that the processing order is deterministic.
const files = await glob(pattern, {
nodir: true,
cwd: directory,
ignore: ignorePattern,
absolute: true,
})
return files.sort()
}

export function uploadPlaywrightProject (dir: string): Promise<AxiosResponse> {
const { size } = fsSync.statSync(dir)
const stream = fsSync.createReadStream(dir)
return checklyStorage.uploadCodeBundle(stream, size)
}

export function cleanup (dir: string) {
if (!dir.length) {
return
}
fsSync.rmSync(dir)
}

function getFiles (dir: string, files: string[] = []) {
const fileList = fsSync.readdirSync(dir)
for (const file of fileList) {
const name = path.join(dir, file)
if (fsSync.statSync(name).isDirectory()) {
getFiles(name, files)
} else {
files.push(name)
}
}
return files
}
Loading