Skip to content

Commit

Permalink
feat: optionally bypass runtime dependency checks [sc-22475] (#986)
Browse files Browse the repository at this point in the history
* feat: optionally bypass runtime dependency checks

Introduces a new option called `--[no-]verify-runtime-dependencies` for the
test and deploy commands, which is true by default and matches current
behavior. Should the user decide that they know the available dependencies
better than we do, they can run the commands with the
`--no-verify-runtime-dependencies` flag set, or they can use the equivalent
`CHECKLY_VERIFY_RUNTIME_DEPENDENCIES=0` environment variable.

Specifically, this feature makes it possible to use custom dependencies that
have been added to a customized private location runtime (contact your
Account Executive to learn how).

* feat: update tests

* fix: enable verifyRuntimeDependencies if not specified in parser options

Makes it easier to use parseProject in tests.
  • Loading branch information
sorccu authored Nov 29, 2024
1 parent 2fc5bde commit cef23cf
Show file tree
Hide file tree
Showing 9 changed files with 94 additions and 21 deletions.
8 changes: 8 additions & 0 deletions packages/cli/src/commands/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,12 @@ export default class Deploy extends AuthCommand {
char: 'c',
description: commonMessages.configFile,
}),
'verify-runtime-dependencies': Flags.boolean({
description: '[default: true] Return an error if checks import dependencies that are not supported by the selected runtime.',
default: true,
allowNo: true,
env: 'CHECKLY_VERIFY_RUNTIME_DEPENDENCIES',
}),
}

async run (): Promise<void> {
Expand All @@ -66,6 +72,7 @@ export default class Deploy extends AuthCommand {
'schedule-on-deploy': scheduleOnDeploy,
output,
config: configFilename,
'verify-runtime-dependencies': verifyRuntimeDependencies,
} = flags
const { configDirectory, configFilenames } = splitConfigFilePath(configFilename)
const {
Expand All @@ -88,6 +95,7 @@ export default class Deploy extends AuthCommand {
acc[runtime.name] = runtime
return acc
}, <Record<string, Runtime>> {}),
verifyRuntimeDependencies,
checklyConfigConstructs,
})
const repoInfo = getGitInformation(project.repoUrl)
Expand Down
8 changes: 8 additions & 0 deletions packages/cli/src/commands/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,12 @@ export default class Test extends AuthCommand {
retries: Flags.integer({
description: `[default: 0, max: ${MAX_RETRIES}] How many times to retry a failing test run.`,
}),
'verify-runtime-dependencies': Flags.boolean({
description: '[default: true] Return an error if checks import dependencies that are not supported by the selected runtime.',
default: true,
allowNo: true,
env: 'CHECKLY_VERIFY_RUNTIME_DEPENDENCIES',
}),
}

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

Expand Down Expand Up @@ -169,6 +176,7 @@ export default class Test extends AuthCommand {
acc[runtime.name] = runtime
return acc
}, <Record<string, Runtime>> {}),
verifyRuntimeDependencies,
checklyConfigConstructs,
})
const checks = Object.entries(project.data.check)
Expand Down
5 changes: 4 additions & 1 deletion packages/cli/src/constructs/api-check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -352,7 +352,10 @@ export class ApiCheck extends Check {
if (!runtime) {
throw new Error(`${runtimeId} is not supported`)
}
const parser = new Parser(Object.keys(runtime.dependencies))
const parser = new Parser({
supportedNpmModules: Object.keys(runtime.dependencies),
checkUnsupportedModules: Session.verifyRuntimeDependencies,
})
const parsed = parser.parse(absoluteEntrypoint)
// Maybe we can get the parsed deps with the content immediately

Expand Down
5 changes: 4 additions & 1 deletion packages/cli/src/constructs/browser-check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,10 @@ export class BrowserCheck extends Check {
if (!runtime) {
throw new Error(`${runtimeId} is not supported`)
}
const parser = new Parser(Object.keys(runtime.dependencies))
const parser = new Parser({
supportedNpmModules: Object.keys(runtime.dependencies),
checkUnsupportedModules: Session.verifyRuntimeDependencies,
})
const parsed = parser.parse(entry)
// Maybe we can get the parsed deps with the content immediately

Expand Down
5 changes: 4 additions & 1 deletion packages/cli/src/constructs/multi-step-check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,10 @@ export class MultiStepCheck extends Check {
if (!runtime) {
throw new Error(`${runtimeId} is not supported`)
}
const parser = new Parser(Object.keys(runtime.dependencies))
const parser = new Parser({
supportedNpmModules: Object.keys(runtime.dependencies),
checkUnsupportedModules: Session.verifyRuntimeDependencies,
})
const parsed = parser.parse(entry)
// Maybe we can get the parsed deps with the content immediately

Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/constructs/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ export class Session {
static checkFilePath?: string
static checkFileAbsolutePath?: string
static availableRuntimes: Record<string, Runtime>
static verifyRuntimeDependencies = true
static loadingChecklyConfigFile: boolean
static checklyConfigFileConstructs?: Construct[]
static privateLocations: PrivateLocationApi[]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,18 @@ const defaultNpmModules = [

describe('dependency-parser - parser()', () => {
it('should handle JS file with no dependencies', () => {
const parser = new Parser(defaultNpmModules)
const parser = new Parser({
supportedNpmModules: defaultNpmModules,
})
const { dependencies } = parser.parse(path.join(__dirname, 'check-parser-fixtures', 'no-dependencies.js'))
expect(dependencies.map(d => d.filePath)).toHaveLength(0)
})

it('should handle JS file with dependencies', () => {
const toAbsolutePath = (...filepath: string[]) => path.join(__dirname, 'check-parser-fixtures', 'simple-example', ...filepath)
const parser = new Parser(defaultNpmModules)
const parser = new Parser({
supportedNpmModules: defaultNpmModules,
})
const { dependencies } = parser.parse(toAbsolutePath('entrypoint.js'))
expect(dependencies.map(d => d.filePath).sort()).toEqual([
toAbsolutePath('dep1.js'),
Expand All @@ -31,7 +35,9 @@ describe('dependency-parser - parser()', () => {
it('should report a missing entrypoint file', () => {
const missingEntrypoint = path.join(__dirname, 'check-parser-fixtures', 'does-not-exist.js')
try {
const parser = new Parser(defaultNpmModules)
const parser = new Parser({
supportedNpmModules: defaultNpmModules,
})
parser.parse(missingEntrypoint)
} catch (err) {
expect(err).toMatchObject({ missingFiles: [missingEntrypoint] })
Expand All @@ -41,7 +47,9 @@ describe('dependency-parser - parser()', () => {
it('should report missing check dependencies', () => {
const toAbsolutePath = (...filepath: string[]) => path.join(__dirname, 'check-parser-fixtures', ...filepath)
try {
const parser = new Parser(defaultNpmModules)
const parser = new Parser({
supportedNpmModules: defaultNpmModules,
})
parser.parse(toAbsolutePath('missing-dependencies.js'))
} catch (err) {
expect(err).toMatchObject({ missingFiles: [toAbsolutePath('does-not-exist.js'), toAbsolutePath('does-not-exist2.js')] })
Expand All @@ -51,7 +59,9 @@ describe('dependency-parser - parser()', () => {
it('should report syntax errors', () => {
const entrypoint = path.join(__dirname, 'check-parser-fixtures', 'syntax-error.js')
try {
const parser = new Parser(defaultNpmModules)
const parser = new Parser({
supportedNpmModules: defaultNpmModules,
})
parser.parse(entrypoint)
} catch (err) {
expect(err).toMatchObject({ parseErrors: [{ file: entrypoint, error: 'Unexpected token (4:70)' }] })
Expand All @@ -61,16 +71,29 @@ describe('dependency-parser - parser()', () => {
it('should report unsupported dependencies', () => {
const entrypoint = path.join(__dirname, 'check-parser-fixtures', 'unsupported-dependencies.js')
try {
const parser = new Parser(defaultNpmModules)
const parser = new Parser({
supportedNpmModules: defaultNpmModules,
})
parser.parse(entrypoint)
} catch (err) {
expect(err).toMatchObject({ unsupportedNpmDependencies: [{ file: entrypoint, unsupportedDependencies: ['left-pad', 'right-pad'] }] })
}
})

it('should allow unsupported dependencies if configured to do so', () => {
const entrypoint = path.join(__dirname, 'check-parser-fixtures', 'unsupported-dependencies.js')
const parser = new Parser({
supportedNpmModules: defaultNpmModules,
checkUnsupportedModules: false,
})
parser.parse(entrypoint)
})

it('should handle circular dependencies', () => {
const toAbsolutePath = (...filepath: string[]) => path.join(__dirname, 'check-parser-fixtures', 'circular-dependencies', ...filepath)
const parser = new Parser(defaultNpmModules)
const parser = new Parser({
supportedNpmModules: defaultNpmModules,
})
const { dependencies } = parser.parse(toAbsolutePath('entrypoint.js'))

// Circular dependencies are allowed in Node.js
Expand All @@ -84,7 +107,9 @@ describe('dependency-parser - parser()', () => {

it('should parse typescript dependencies', () => {
const toAbsolutePath = (...filepath: string[]) => path.join(__dirname, 'check-parser-fixtures', 'typescript-example', ...filepath)
const parser = new Parser(defaultNpmModules)
const parser = new Parser({
supportedNpmModules: defaultNpmModules,
})
const { dependencies } = parser.parse(toAbsolutePath('entrypoint.ts'))
expect(dependencies.map(d => d.filePath).sort()).toEqual([
toAbsolutePath('dep1.ts'),
Expand All @@ -102,7 +127,9 @@ describe('dependency-parser - parser()', () => {

it('should handle ES Modules', () => {
const toAbsolutePath = (...filepath: string[]) => path.join(__dirname, 'check-parser-fixtures', 'esmodules-example', ...filepath)
const parser = new Parser(defaultNpmModules)
const parser = new Parser({
supportedNpmModules: defaultNpmModules,
})
const { dependencies } = parser.parse(toAbsolutePath('entrypoint.js'))
expect(dependencies.map(d => d.filePath).sort()).toEqual([
toAbsolutePath('dep1.js'),
Expand All @@ -113,7 +140,9 @@ describe('dependency-parser - parser()', () => {

it('should handle Common JS and ES Modules', () => {
const toAbsolutePath = (...filepath: string[]) => path.join(__dirname, 'check-parser-fixtures', 'common-esm-example', ...filepath)
const parser = new Parser(defaultNpmModules)
const parser = new Parser({
supportedNpmModules: defaultNpmModules,
})
const { dependencies } = parser.parse(toAbsolutePath('entrypoint.mjs'))
expect(dependencies.map(d => d.filePath).sort()).toEqual([
toAbsolutePath('dep1.js'),
Expand All @@ -130,21 +159,27 @@ describe('dependency-parser - parser()', () => {
*/
it.skip('should ignore cases where require is reassigned', () => {
const entrypoint = path.join(__dirname, 'check-parser-fixtures', 'reassign-require.js')
const parser = new Parser(defaultNpmModules)
const parser = new Parser({
supportedNpmModules: defaultNpmModules,
})
parser.parse(entrypoint)
})

// Checks run on Checkly are wrapped to support top level await.
// For consistency with checks created via the UI, the CLI should support this as well.
it('should allow top-level await', () => {
const entrypoint = path.join(__dirname, 'check-parser-fixtures', 'top-level-await.js')
const parser = new Parser(defaultNpmModules)
const parser = new Parser({
supportedNpmModules: defaultNpmModules,
})
parser.parse(entrypoint)
})

it('should allow top-level await in TypeScript', () => {
const entrypoint = path.join(__dirname, 'check-parser-fixtures', 'top-level-await.ts')
const parser = new Parser(defaultNpmModules)
const parser = new Parser({
supportedNpmModules: defaultNpmModules,
})
parser.parse(entrypoint)
})
})
19 changes: 14 additions & 5 deletions packages/cli/src/services/check-parser/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,13 +85,20 @@ function getTsParser (): any {
}
}

type ParserOptions = {
supportedNpmModules?: Array<string>
checkUnsupportedModules?: boolean
}

export class Parser {
supportedModules: Set<string>
checkUnsupportedModules: boolean

// TODO: pass a npm matrix of supported npm modules
// Maybe pass a cache so we don't have to fetch files separately all the time
constructor (supportedNpmModules: Array<string>) {
this.supportedModules = new Set([...supportedBuiltinModules, ...supportedNpmModules])
constructor (options: ParserOptions) {
this.supportedModules = new Set(supportedBuiltinModules.concat(options.supportedNpmModules ?? []))
this.checkUnsupportedModules = options.checkUnsupportedModules ?? true
}

parse (entrypoint: string) {
Expand Down Expand Up @@ -120,9 +127,11 @@ export class Parser {
collector.addParsingError(item.filePath, error.message)
continue
}
const unsupportedDependencies = module.npmDependencies.filter((dep) => !this.supportedModules.has(dep))
if (unsupportedDependencies.length) {
collector.addUnsupportedNpmDependencies(item.filePath, unsupportedDependencies)
if (this.checkUnsupportedModules) {
const unsupportedDependencies = module.npmDependencies.filter((dep) => !this.supportedModules.has(dep))
if (unsupportedDependencies.length) {
collector.addUnsupportedNpmDependencies(item.filePath, unsupportedDependencies)
}
}
const localDependenciesResolvedPaths: Array<{filePath: string, content: string}> = []
module.localDependencies.forEach((localDependency: string) => {
Expand Down
3 changes: 3 additions & 0 deletions packages/cli/src/services/project-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ type ProjectParseOpts = {
checkDefaults?: CheckConfigDefaults,
browserCheckDefaults?: CheckConfigDefaults,
availableRuntimes: Record<string, Runtime>,
verifyRuntimeDependencies?: boolean,
checklyConfigConstructs?: Construct[],
}

Expand All @@ -43,6 +44,7 @@ export async function parseProject (opts: ProjectParseOpts): Promise<Project> {
checkDefaults = {},
browserCheckDefaults = {},
availableRuntimes,
verifyRuntimeDependencies,
checklyConfigConstructs,
} = opts
const project = new Project(projectLogicalId, {
Expand All @@ -57,6 +59,7 @@ export async function parseProject (opts: ProjectParseOpts): Promise<Project> {
Session.checkDefaults = Object.assign({}, BASE_CHECK_DEFAULTS, checkDefaults)
Session.browserCheckDefaults = browserCheckDefaults
Session.availableRuntimes = availableRuntimes
Session.verifyRuntimeDependencies = verifyRuntimeDependencies ?? true

// TODO: Do we really need all of the ** globs, or could we just put node_modules?
const ignoreDirectories = ['**/node_modules/**', '**/.git/**', ...ignoreDirectoriesMatch]
Expand Down

0 comments on commit cef23cf

Please sign in to comment.