Skip to content
Draft
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
5 changes: 5 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,11 @@ jobs:
- run: pnpm install --frozen-lockfile
# Test that the packaging works as well
- run: pnpm -r pack
# Startup-time budget: catches new import-time work (e.g. a dependency
# doing real work at module load) that would otherwise silently slow
# every invocation without failing any test. Generous ceiling — this is
# a regression alarm, not a benchmark.
- run: pnpm --filter checkly run startup:check
- run: pnpm run lint

# ---------------------------------------------------------------------------
Expand Down
3 changes: 3 additions & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ export default defineConfig([
'**/syntax-error*',
'examples/*',
'benchmarks/*',
// Scratch space for agents (benchmarks, screenshots, attachments);
// gitignored and not part of any package.
'**/.context/',
'**/*fixtures/',
]),
{
Expand Down
20 changes: 9 additions & 11 deletions packages/cli/bin/launch.cjs
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
'use strict'

// Required by ./run.cjs only after the Node version preflight has passed.
// Everything is loaded with dynamic import() so this stays a CommonJS module:
// require() of the ESM dist or @oclif/core would throw
// ERR_REQUIRE_ASYNC_MODULE if any module in the graph ever adopted top-level
// await. Loading dotenv before @oclif/core also means .env values are visible
// to env vars read at import time (e.g. DEBUG, FORCE_COLOR).
// The CLI is the esbuild bundle at dist/bundle/cli.js (see scripts/bundle.mjs):
// it includes @oclif/core, all commands and every pure-JS dependency. Loading
// it with dynamic import() keeps this file CommonJS. oclif later dynamic-imports
// the same bundle path to resolve commands, which the Node module cache serves
// from this load, so there is exactly one instance of every module. External
// dependencies resolved at runtime (oxc-parser, typescript-estree) are looked
// up from this package's node_modules exactly as before.
void (async () => {
const { loadDotenvFile } = await import('../dist/services/load-dotenv.js')
await loadDotenvFile()

const { execute, settings } = await import('@oclif/core')
settings.enableAutoTranspile = false
await execute({ dir: __dirname })
const { default: runChecklyCli } = await import('../dist/bundle/cli.js')
await runChecklyCli()
})()
14 changes: 14 additions & 0 deletions packages/cli/bin/run.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,18 @@
process.exit(1)
}

// Cache V8 bytecode for the CLI bundle across runs (Node >= 22.8), so warm
// runs skip re-parsing the bundle entirely. Best effort: a no-op when the
// runtime does not provide it or the cache directory is not writable.
// Old-syntax only (var, no optional chaining): like the rest of this file,
// it must parse on runtimes old enough to need the version message above.
/* eslint-disable no-var */
try {
var nodeModule = require('node:module')
if (nodeModule && typeof nodeModule.enableCompileCache === 'function') {
nodeModule.enableCompileCache()
}
// eslint-disable-next-line no-empty
} catch (err) {} /* eslint-enable no-var */

Check warning on line 44 in packages/cli/bin/run.cjs

View workflow job for this annotation

GitHub Actions / lint

'err' is defined but never used

require('./launch.cjs')
2 changes: 1 addition & 1 deletion packages/cli/constructs.js
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
// eslint-disable-next-line
module.exports = require('./dist').constructs
module.exports = require('./dist/bundle/index.js').constructs
93 changes: 56 additions & 37 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,30 @@
"version": "0.0.1-dev",
"type": "module",
"description": "Checkly CLI",
"main": "dist/index.js",
"main": "dist/bundle/index.js",
"types": "dist/index.d.ts",
"exports": {
".": "./dist/index.js",
"./constructs": "./dist/constructs/index.js",
"./loader": "./dist/loader/index.js",
"./util": "./dist/util/index.js"
".": {
"types": "./dist/index.d.ts",
"import": "./dist/bundle/index.js",
"require": "./dist/bundle/index.js"
},
"./package.json": "./package.json",
"./constructs": {
"types": "./dist/constructs/index.d.ts",
"import": "./dist/bundle/constructs.js",
"require": "./dist/bundle/constructs.js"
},
"./loader": {
"types": "./dist/loader/index.d.ts",
"import": "./dist/bundle/loader.js",
"require": "./dist/bundle/loader.js"
},
"./util": {
"types": "./dist/util/index.d.ts",
"import": "./dist/bundle/util.js",
"require": "./dist/bundle/util.js"
}
},
"engines": {
"node": ">=22.13.0"
Expand All @@ -20,7 +37,9 @@
"clean": "pnpm run clean:dist && pnpm run clean:gen",
"prepack": "pnpm exec oclif manifest",
"prepare:ai-context": "cross-env CHECKLY_SKIP_AUTH=1 CHECKLY_E2E_CLI_VERSION=99.0.0 ./bin/run.cjs import plan --root gen --debug-import-plan-input-file ./src/ai-context/context.fixtures.json && jiti ./scripts/prepare-ai-context.ts",
"prepare:dist": "tsc --build",
"bundle": "node scripts/bundle.mjs",
"startup:check": "node scripts/startup-check.mjs",
"prepare:dist": "tsc --build && pnpm run bundle",
"prepare": "pnpm run clean && pnpm run prepare:dist && pnpm run prepare:ai-context",
"test": "pnpm pack && pnpm run test:types && vitest --run",
"test:types": "tsc -p tsconfig.type-tests.json --noEmit",
Expand All @@ -40,14 +59,17 @@
"files": [
"/bin",
"/dist",
"!/dist/**/*.js",
"!/dist/**/*.js.map",
"/dist/bundle",
"/assets",
"/constructs.js",
"/constructs.d.ts",
"/oclif.manifest.json"
],
"oclif": {
"bin": "checkly",
"commands": "./dist/commands",
"commands": "./dist/bundle/commands",
"topicSeparator": " ",
"additionalHelpFlags": [
"-h"
Expand All @@ -56,13 +78,10 @@
"-v",
"version"
],
"plugins": [
"@oclif/plugin-help",
"@oclif/plugin-warn-if-update-available"
],
"hooks": {
"command_not_found": "./dist/hooks/command-not-found",
"finally": "./dist/hooks/parse-error"
"init": "./dist/bundle/hooks/check-update.js",
"command_not_found": "./dist/bundle/hooks/command-not-found.js",
"finally": "./dist/bundle/hooks/parse-error.js"
},
"warn-if-update-available": {
"timeoutInDays": 30,
Expand Down Expand Up @@ -106,16 +125,29 @@
"description": "Inspect recorded test sessions."
}
},
"helpClass": "./dist/help/help-extension.js"
"helpClass": "./dist/bundle/help/help-extension.js"
},
"bin": {
"checkly": "./bin/run.cjs"
},
"homepage": "https://github.com/checkly/checkly-cli#readme",
"dependencies": {
"jiti": "^2.7.0",
"oxc-parser": "0.153.0"
},
"devDependencies": {
"@oclif/core": "^5.1.7",
"@oclif/plugin-help": "^7.0.4",
"@oclif/plugin-warn-if-update-available": "^4.0.3",
"@playwright/test": "^1.64.0",
"@types/archiver": "^8.0.0",
"@types/debug": "^4.1.13",
"@types/luxon": "^3.7.6",
"@types/node": "^22.19.17",
"@types/prompts": "^2.4.9",
"@types/semver": "^7.8.0",
"@types/ws": "^8.18.2",
"@typescript-eslint/types": "^8.71.1",
"@typescript-eslint/typescript-estree": "^8.71.1",
"acorn": "^8.19.0",
"acorn-walk": "^8.3.5",
"archiver": "^8.0.0",
Expand All @@ -124,52 +156,39 @@
"chalk": "^6.0.1",
"ci-info": "^4.4.0",
"conf": "^15.1.0",
"config": "^5.0.1",
"cross-env": "^10.1.0",
"debug": "^4.4.3",
"dotenv": "^18.0.7",
"esbuild": "^0.28.2",
"execa": "^10.1.0",
"git-repo-info": "^2.1.1",
"glob": "^13.0.6",
"http-proxy-agent": "^9.1.0",
"https-proxy-agent": "^9.1.0",
"indent-string": "^5.0.0",
"jiti": "^2.7.0",
"json-stream-stringify": "^3.1.7",
"json5": "^2.2.3",
"jwt-decode": "^4.0.0",
"log-symbols": "^7.0.1",
"luxon": "^3.7.2",
"minimatch": "^10.2.6",
"mqtt": "^5.16.0",
"nanoid": "^6.0.2",
"oclif": "^6.0.8",
"open": "^11.0.4",
"oxc-parser": "0.153.0",
"p-queue": "^9.3.4",
"prompts": "^2.4.2",
"proxy-from-env": "^2.1.0",
"recast": "^0.24.0",
"rimraf": "^6.1.3",
"semver": "^7.8.5",
"socks-proxy-agent": "^10.1.0",
"string-width": "^8.3.0",
"uuid": "^14.0.3",
"yaml": "^2.9.1"
},
"devDependencies": {
"@playwright/test": "^1.64.0",
"@types/archiver": "^8.0.0",
"@types/debug": "^4.1.13",
"@types/luxon": "^3.7.6",
"@types/node": "^22.19.17",
"@types/prompts": "^2.4.9",
"@types/semver": "^7.8.0",
"@types/ws": "^8.18.2",
"@typescript-eslint/types": "^8.71.1",
"@typescript-eslint/typescript-estree": "^8.71.1",
"config": "^5.0.1",
"cross-env": "^10.1.0",
"nanoid": "^6.0.2",
"oclif": "^6.0.8",
"rimraf": "^6.1.3",
"tar": "^7.5.22",
"typescript": "^6.0.3",
"vitest": "^3.2.7"
"uuid": "^14.0.3",
"vitest": "^3.2.7",
"yaml": "^2.9.1"
}
}
118 changes: 118 additions & 0 deletions packages/cli/scripts/bundle.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
#!/usr/bin/env node
/**
* Bundles the compiled tsc output in dist/ into dist/bundle/ (a Shopify-style
* bundle, adapted for lazy command loading):
*
* - One ESM entry per oclif command (mirroring dist/commands/**), plus small
* entries for the CLI bootstrap, hooks, the help class and the public API
* surface. esbuild code splitting puts shared modules in dist/bundle/chunks,
* so a command entry only loads the chunks it actually uses — a run of
* `checkly env ls` never evaluates the mqtt or archiver graphs.
* - oclif discovers commands with its default pattern strategy from
* dist/bundle/commands (see oclif.commands in package.json), exactly as it
* did from dist/commands before bundling. Loading stays lazy: no command
* module is evaluated until one is actually needed.
*
* Only native or resolution-sensitive dependencies stay external:
* - oxc-parser: Rust native bindings resolved per platform at runtime.
* - jiti: lazy-requires sibling files from its own package at runtime
* (e.g. dist/babel.cjs), which cannot be satisfied from a bundle.
* - typescript: never bundled (only resolved from a user's project, if at
* all, since write-back moved to oxc-parser).
*/
import { build } from 'esbuild'
import { readdirSync, rmSync, mkdirSync } from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'

const __dirname = path.dirname(fileURLToPath(import.meta.url))
const pkgRoot = path.resolve(__dirname, '..')
const outDir = path.join(pkgRoot, 'dist', 'bundle')

function walk (dir, base = dir) {
const entries = []
for (const entry of readdirSync(dir, { withFileTypes: true })) {
if (entry.isDirectory()) {
if (entry.name === '__tests__') continue
entries.push(...walk(path.join(dir, entry.name), base))
} else if (entry.name.endsWith('.js') && !entry.name.endsWith('.d.js') && !entry.name.endsWith('.js.map')) {
entries.push(path.join(dir, entry.name))
}
}
return entries
}

const commandFiles = walk(path.join(pkgRoot, 'dist', 'commands'))

const entryPoints = [
// The CLI bootstrap that bin/launch.cjs imports (dotenv + oclif execute).
{ in: 'dist/cli.js', out: 'cli' },
// Hooks and the help class, wired up by oclif.hooks / oclif.helpClass in
// package.json.
{ in: 'dist/hooks/command-not-found.js', out: 'hooks/command-not-found' },
{ in: 'dist/hooks/parse-error.js', out: 'hooks/parse-error' },
{ in: 'dist/hooks/check-update.js', out: 'hooks/check-update' },
{ in: 'dist/help/help-extension.js', out: 'help/help-extension' },
// The public API surface (package.json "exports").
{ in: 'dist/index.js', out: 'index' },
{ in: 'dist/constructs/index.js', out: 'constructs' },
{ in: 'dist/loader/index.js', out: 'loader' },
{ in: 'dist/util/index.js', out: 'util' },
// One entry per command, mirroring the dist/commands layout so oclif's
// pattern strategy derives the same command IDs as before bundling.
...commandFiles.map(file => ({
in: path.relative(pkgRoot, file),
out: path.join('commands', path.relative(path.join(pkgRoot, 'dist', 'commands'), file).replace(/\.js$/, '')),
})),
]

rmSync(outDir, { recursive: true, force: true })
mkdirSync(outDir, { recursive: true })

const result = await build({
absWorkingDir: pkgRoot,
entryPoints,
bundle: true,
format: 'esm',
platform: 'node',
target: 'node22.13',
splitting: true,
outdir: 'dist/bundle',
// Shared code goes to dist/bundle/chunks/ so oclif's command glob over
// dist/bundle/commands only ever sees command entries.
chunkNames: 'chunks/[name]-[hash]',
// Runtime code relies on Function.name: the deploy preview prints
// construct.constructor.name (e.g. "ApiCheck"), and error messages and
// write-back checks name construct classes too. Without keepNames, esbuild
// renames classes whose name collides with their export binding in chunk
// scope (ApiCheck = class _ApiCheck), which leaks the renamed identifier
// into user-visible output. keepNames restores the source names.
keepNames: true,
// Minify: roughly halves the bundle (7.4 MB -> 3.5 MB, tarball 1.69 ->
// 1.32 MB gz) and keepNames keeps class and function names readable in
// stack traces; only local identifiers are mangled. Sourcemaps are not
// shipped: the split-bundle maps weigh ~13 MB, and Node only applies
// sourceMappingURL with --enable-source-maps anyway.
minify: true,
// Bundled CJS dependencies (e.g. supports-color via debug) call require()
// for node builtins at runtime. esbuild's ESM output can only resolve those
// when a require function exists in scope; this banner supplies one.
banner: {
js: [
'import { createRequire as __bannerCreateRequire } from \'node:module\'',
'const require = __bannerCreateRequire(import.meta.url)',
].join('\n'),
},
external: ['oxc-parser', '@typescript-eslint/typescript-estree', 'jiti', 'typescript'],
metafile: true,
logLevel: 'warning',
})

let commandEntriesCount = 0
let bytes = 0
for (const [file, meta] of Object.entries(result.metafile.outputs)) {
bytes += meta.bytes
if (file.includes('/commands/')) commandEntriesCount++
}
// eslint-disable-next-line no-console
console.log(`Bundle: ${entryPoints.length} entries (${commandEntriesCount} commands), ${Object.keys(result.metafile.outputs).length} files, ${(bytes / 1e6).toFixed(1)} MB`)
Loading
Loading