Skip to content

Commit cef23cf

Browse files
authored
feat: optionally bypass runtime dependency checks [sc-22475] (#986)
* 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.
1 parent 2fc5bde commit cef23cf

File tree

9 files changed

+94
-21
lines changed

9 files changed

+94
-21
lines changed

packages/cli/src/commands/deploy.ts

+8
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,12 @@ export default class Deploy extends AuthCommand {
5555
char: 'c',
5656
description: commonMessages.configFile,
5757
}),
58+
'verify-runtime-dependencies': Flags.boolean({
59+
description: '[default: true] Return an error if checks import dependencies that are not supported by the selected runtime.',
60+
default: true,
61+
allowNo: true,
62+
env: 'CHECKLY_VERIFY_RUNTIME_DEPENDENCIES',
63+
}),
5864
}
5965

6066
async run (): Promise<void> {
@@ -66,6 +72,7 @@ export default class Deploy extends AuthCommand {
6672
'schedule-on-deploy': scheduleOnDeploy,
6773
output,
6874
config: configFilename,
75+
'verify-runtime-dependencies': verifyRuntimeDependencies,
6976
} = flags
7077
const { configDirectory, configFilenames } = splitConfigFilePath(configFilename)
7178
const {
@@ -88,6 +95,7 @@ export default class Deploy extends AuthCommand {
8895
acc[runtime.name] = runtime
8996
return acc
9097
}, <Record<string, Runtime>> {}),
98+
verifyRuntimeDependencies,
9199
checklyConfigConstructs,
92100
})
93101
const repoInfo = getGitInformation(project.repoUrl)

packages/cli/src/commands/test.ts

+8
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,12 @@ export default class Test extends AuthCommand {
104104
retries: Flags.integer({
105105
description: `[default: 0, max: ${MAX_RETRIES}] How many times to retry a failing test run.`,
106106
}),
107+
'verify-runtime-dependencies': Flags.boolean({
108+
description: '[default: true] Return an error if checks import dependencies that are not supported by the selected runtime.',
109+
default: true,
110+
allowNo: true,
111+
env: 'CHECKLY_VERIFY_RUNTIME_DEPENDENCIES',
112+
}),
107113
}
108114

109115
static args = {
@@ -137,6 +143,7 @@ export default class Test extends AuthCommand {
137143
'test-session-name': testSessionName,
138144
'update-snapshots': updateSnapshots,
139145
retries,
146+
'verify-runtime-dependencies': verifyRuntimeDependencies,
140147
} = flags
141148
const filePatterns = argv as string[]
142149

@@ -169,6 +176,7 @@ export default class Test extends AuthCommand {
169176
acc[runtime.name] = runtime
170177
return acc
171178
}, <Record<string, Runtime>> {}),
179+
verifyRuntimeDependencies,
172180
checklyConfigConstructs,
173181
})
174182
const checks = Object.entries(project.data.check)

packages/cli/src/constructs/api-check.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -352,7 +352,10 @@ export class ApiCheck extends Check {
352352
if (!runtime) {
353353
throw new Error(`${runtimeId} is not supported`)
354354
}
355-
const parser = new Parser(Object.keys(runtime.dependencies))
355+
const parser = new Parser({
356+
supportedNpmModules: Object.keys(runtime.dependencies),
357+
checkUnsupportedModules: Session.verifyRuntimeDependencies,
358+
})
356359
const parsed = parser.parse(absoluteEntrypoint)
357360
// Maybe we can get the parsed deps with the content immediately
358361

packages/cli/src/constructs/browser-check.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,10 @@ export class BrowserCheck extends Check {
120120
if (!runtime) {
121121
throw new Error(`${runtimeId} is not supported`)
122122
}
123-
const parser = new Parser(Object.keys(runtime.dependencies))
123+
const parser = new Parser({
124+
supportedNpmModules: Object.keys(runtime.dependencies),
125+
checkUnsupportedModules: Session.verifyRuntimeDependencies,
126+
})
124127
const parsed = parser.parse(entry)
125128
// Maybe we can get the parsed deps with the content immediately
126129

packages/cli/src/constructs/multi-step-check.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,10 @@ export class MultiStepCheck extends Check {
104104
if (!runtime) {
105105
throw new Error(`${runtimeId} is not supported`)
106106
}
107-
const parser = new Parser(Object.keys(runtime.dependencies))
107+
const parser = new Parser({
108+
supportedNpmModules: Object.keys(runtime.dependencies),
109+
checkUnsupportedModules: Session.verifyRuntimeDependencies,
110+
})
108111
const parsed = parser.parse(entry)
109112
// Maybe we can get the parsed deps with the content immediately
110113

packages/cli/src/constructs/project.ts

+1
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ export class Session {
142142
static checkFilePath?: string
143143
static checkFileAbsolutePath?: string
144144
static availableRuntimes: Record<string, Runtime>
145+
static verifyRuntimeDependencies = true
145146
static loadingChecklyConfigFile: boolean
146147
static checklyConfigFileConstructs?: Construct[]
147148
static privateLocations: PrivateLocationApi[]

packages/cli/src/services/check-parser/__tests__/check-parser.spec.ts

+48-13
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,18 @@ const defaultNpmModules = [
99

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

1719
it('should handle JS file with dependencies', () => {
1820
const toAbsolutePath = (...filepath: string[]) => path.join(__dirname, 'check-parser-fixtures', 'simple-example', ...filepath)
19-
const parser = new Parser(defaultNpmModules)
21+
const parser = new Parser({
22+
supportedNpmModules: defaultNpmModules,
23+
})
2024
const { dependencies } = parser.parse(toAbsolutePath('entrypoint.js'))
2125
expect(dependencies.map(d => d.filePath).sort()).toEqual([
2226
toAbsolutePath('dep1.js'),
@@ -31,7 +35,9 @@ describe('dependency-parser - parser()', () => {
3135
it('should report a missing entrypoint file', () => {
3236
const missingEntrypoint = path.join(__dirname, 'check-parser-fixtures', 'does-not-exist.js')
3337
try {
34-
const parser = new Parser(defaultNpmModules)
38+
const parser = new Parser({
39+
supportedNpmModules: defaultNpmModules,
40+
})
3541
parser.parse(missingEntrypoint)
3642
} catch (err) {
3743
expect(err).toMatchObject({ missingFiles: [missingEntrypoint] })
@@ -41,7 +47,9 @@ describe('dependency-parser - parser()', () => {
4147
it('should report missing check dependencies', () => {
4248
const toAbsolutePath = (...filepath: string[]) => path.join(__dirname, 'check-parser-fixtures', ...filepath)
4349
try {
44-
const parser = new Parser(defaultNpmModules)
50+
const parser = new Parser({
51+
supportedNpmModules: defaultNpmModules,
52+
})
4553
parser.parse(toAbsolutePath('missing-dependencies.js'))
4654
} catch (err) {
4755
expect(err).toMatchObject({ missingFiles: [toAbsolutePath('does-not-exist.js'), toAbsolutePath('does-not-exist2.js')] })
@@ -51,7 +59,9 @@ describe('dependency-parser - parser()', () => {
5159
it('should report syntax errors', () => {
5260
const entrypoint = path.join(__dirname, 'check-parser-fixtures', 'syntax-error.js')
5361
try {
54-
const parser = new Parser(defaultNpmModules)
62+
const parser = new Parser({
63+
supportedNpmModules: defaultNpmModules,
64+
})
5565
parser.parse(entrypoint)
5666
} catch (err) {
5767
expect(err).toMatchObject({ parseErrors: [{ file: entrypoint, error: 'Unexpected token (4:70)' }] })
@@ -61,16 +71,29 @@ describe('dependency-parser - parser()', () => {
6171
it('should report unsupported dependencies', () => {
6272
const entrypoint = path.join(__dirname, 'check-parser-fixtures', 'unsupported-dependencies.js')
6373
try {
64-
const parser = new Parser(defaultNpmModules)
74+
const parser = new Parser({
75+
supportedNpmModules: defaultNpmModules,
76+
})
6577
parser.parse(entrypoint)
6678
} catch (err) {
6779
expect(err).toMatchObject({ unsupportedNpmDependencies: [{ file: entrypoint, unsupportedDependencies: ['left-pad', 'right-pad'] }] })
6880
}
6981
})
7082

83+
it('should allow unsupported dependencies if configured to do so', () => {
84+
const entrypoint = path.join(__dirname, 'check-parser-fixtures', 'unsupported-dependencies.js')
85+
const parser = new Parser({
86+
supportedNpmModules: defaultNpmModules,
87+
checkUnsupportedModules: false,
88+
})
89+
parser.parse(entrypoint)
90+
})
91+
7192
it('should handle circular dependencies', () => {
7293
const toAbsolutePath = (...filepath: string[]) => path.join(__dirname, 'check-parser-fixtures', 'circular-dependencies', ...filepath)
73-
const parser = new Parser(defaultNpmModules)
94+
const parser = new Parser({
95+
supportedNpmModules: defaultNpmModules,
96+
})
7497
const { dependencies } = parser.parse(toAbsolutePath('entrypoint.js'))
7598

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

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

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

114141
it('should handle Common JS and ES Modules', () => {
115142
const toAbsolutePath = (...filepath: string[]) => path.join(__dirname, 'check-parser-fixtures', 'common-esm-example', ...filepath)
116-
const parser = new Parser(defaultNpmModules)
143+
const parser = new Parser({
144+
supportedNpmModules: defaultNpmModules,
145+
})
117146
const { dependencies } = parser.parse(toAbsolutePath('entrypoint.mjs'))
118147
expect(dependencies.map(d => d.filePath).sort()).toEqual([
119148
toAbsolutePath('dep1.js'),
@@ -130,21 +159,27 @@ describe('dependency-parser - parser()', () => {
130159
*/
131160
it.skip('should ignore cases where require is reassigned', () => {
132161
const entrypoint = path.join(__dirname, 'check-parser-fixtures', 'reassign-require.js')
133-
const parser = new Parser(defaultNpmModules)
162+
const parser = new Parser({
163+
supportedNpmModules: defaultNpmModules,
164+
})
134165
parser.parse(entrypoint)
135166
})
136167

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

145178
it('should allow top-level await in TypeScript', () => {
146179
const entrypoint = path.join(__dirname, 'check-parser-fixtures', 'top-level-await.ts')
147-
const parser = new Parser(defaultNpmModules)
180+
const parser = new Parser({
181+
supportedNpmModules: defaultNpmModules,
182+
})
148183
parser.parse(entrypoint)
149184
})
150185
})

packages/cli/src/services/check-parser/parser.ts

+14-5
Original file line numberDiff line numberDiff line change
@@ -85,13 +85,20 @@ function getTsParser (): any {
8585
}
8686
}
8787

88+
type ParserOptions = {
89+
supportedNpmModules?: Array<string>
90+
checkUnsupportedModules?: boolean
91+
}
92+
8893
export class Parser {
8994
supportedModules: Set<string>
95+
checkUnsupportedModules: boolean
9096

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

97104
parse (entrypoint: string) {
@@ -120,9 +127,11 @@ export class Parser {
120127
collector.addParsingError(item.filePath, error.message)
121128
continue
122129
}
123-
const unsupportedDependencies = module.npmDependencies.filter((dep) => !this.supportedModules.has(dep))
124-
if (unsupportedDependencies.length) {
125-
collector.addUnsupportedNpmDependencies(item.filePath, unsupportedDependencies)
130+
if (this.checkUnsupportedModules) {
131+
const unsupportedDependencies = module.npmDependencies.filter((dep) => !this.supportedModules.has(dep))
132+
if (unsupportedDependencies.length) {
133+
collector.addUnsupportedNpmDependencies(item.filePath, unsupportedDependencies)
134+
}
126135
}
127136
const localDependenciesResolvedPaths: Array<{filePath: string, content: string}> = []
128137
module.localDependencies.forEach((localDependency: string) => {

packages/cli/src/services/project-parser.ts

+3
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ type ProjectParseOpts = {
2323
checkDefaults?: CheckConfigDefaults,
2424
browserCheckDefaults?: CheckConfigDefaults,
2525
availableRuntimes: Record<string, Runtime>,
26+
verifyRuntimeDependencies?: boolean,
2627
checklyConfigConstructs?: Construct[],
2728
}
2829

@@ -43,6 +44,7 @@ export async function parseProject (opts: ProjectParseOpts): Promise<Project> {
4344
checkDefaults = {},
4445
browserCheckDefaults = {},
4546
availableRuntimes,
47+
verifyRuntimeDependencies,
4648
checklyConfigConstructs,
4749
} = opts
4850
const project = new Project(projectLogicalId, {
@@ -57,6 +59,7 @@ export async function parseProject (opts: ProjectParseOpts): Promise<Project> {
5759
Session.checkDefaults = Object.assign({}, BASE_CHECK_DEFAULTS, checkDefaults)
5860
Session.browserCheckDefaults = browserCheckDefaults
5961
Session.availableRuntimes = availableRuntimes
62+
Session.verifyRuntimeDependencies = verifyRuntimeDependencies ?? true
6063

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

0 commit comments

Comments
 (0)