diff --git a/.claude/unit-testing-jobs.md b/.claude/unit-testing-jobs.md new file mode 100644 index 000000000..8c7db06af --- /dev/null +++ b/.claude/unit-testing-jobs.md @@ -0,0 +1,157 @@ +# Unit Testing Job Code + +OpenFn job expressions are not valid JavaScript out of the box — top-level adaptor calls like `get('/endpoint')` prevent them from being imported directly into a test runner. Compiling them first solves this. + +## Approach + +1. Compile job expressions to standard JavaScript +2. Import compiled files in your test suite +3. Test any pure functions in isolation + +## Compiling for Tests + +`openfn compile` writes compiled output to `dist/` by default (as `.mjs` files). Use `--exports-only` to also strip adaptor operation calls, keeping only explicitly exported code. + +```bash +# Compile all workflows in the project (full compilation, preserves operations) +openfn compile + +# Compile all workflows, stripping operation calls (useful for unit testing) +openfn compile --exports-only + +# Compile a single workflow by name +openfn compile my-workflow + +# Compile a single workflow to a custom directory +openfn compile my-workflow -o tests/ + +# Compile a single job expression (prints to stdout) +openfn compile workflows/my-workflow/step-a.js --exports-only + +# Watch mode: recompile whenever source files change +openfn compile --exports-only --watch + +# Remove the output folder before compiling +openfn compile --exports-only --clean + +# Compile a project in another directory +openfn compile --workspace path/to/project +``` + +## What `--exports-only` keeps + +`--exports-only` strips operation calls. Only explicitly exported declarations survive: + +- `export const myHelper = ...` ✓ +- `export function parseSms() {}` ✓ +- `const helper = ...` (not exported) ✗ — dropped +- `fn(state => ...)` (operation call) ✗ — stripped + +`export default` is removed in strip mode — it is only needed by the runtime, not for unit testing. + +Steps whose compiled output is empty after stripping are skipped — no file is written. + +## Full compilation (no `--exports-only`) + +Without `--exports-only`, the full compiled output is written — all declarations are preserved and operations are kept in `export default [op1, op2, ...]`: + +```js +import { post } from '@openfn/language-http'; + +export default [post('/endpoint', { data: state.data })]; +``` + +All steps are written regardless of whether they export anything. + +## Example: Testing a Helper Function + +**Source** (`workflows/dhis2-sync/transform.js`): + +```js +import { dateFns } from '@openfn/language-dhis2'; + +export const formatDate = (date) => dateFns.format(date, 'yyyy-MM-dd'); + +fn((state) => ({ + ...state, + data: state.data.map((row) => ({ + ...row, + date: formatDate(row.date), + })), +})); +``` + +**Compiled** (`dist/dhis2-sync/transform.mjs`) after `openfn compile --exports-only`: + +```js +import { dateFns } from '@openfn/language-dhis2'; + +export const formatDate = (date) => dateFns.format(date, 'yyyy-MM-dd'); +``` + +The operation is stripped. `formatDate` survives because it is explicitly exported. + +**Test** (using any test runner): + +```js +// test/transform.test.js +import { formatDate } from '../dist/dhis2-sync/transform.mjs'; + +test('formats a date correctly', () => { + const result = formatDate(new Date('2024-01-15')); + assert.equal(result, '2024-01-15'); +}); +``` + +## Project-wide Compilation + +Running `openfn compile` with no path compiles every step in every workflow in the current project directory (must contain `openfn.yaml`). + +Output layout: + +``` +dist/ + my-workflow/ + step-a.mjs + step-b.mjs + another-workflow/ + step-c.mjs +``` + +Override the output directory with `-o `: + +```bash +openfn compile --exports-only -o tests/ +``` + +Configure default directories in `openfn.yaml`: + +```yaml +dirs: + workflows: workflows + compiled: dist # output dir used by openfn compile +``` + +## Recommended Setup + +**`package.json`** (in your OpenFn project): + +```json +{ + "scripts": { + "compile": "openfn compile --exports-only", + "compile:watch": "openfn compile --exports-only --watch", + "test": "node --test test/**/*.test.js" + } +} +``` + +## Notes + +- In `--exports-only` mode, only `export const` and `export function` declarations survive — non-exported helpers are always dropped +- Import statements are always preserved +- Without `--exports-only`, all code including operations is kept in `export default [...]` +- Watch mode reruns compilation on any source change, making the edit → test cycle fast +- Use `-O` to print compiled output to stdout instead of writing to disk +- Output files use the `.mjs` extension, so Node always treats them as ES modules — no `"type": "module"` needed in your project's `package.json` +- Use `--clean` to remove the output folder before compiling, and `--workspace ` to target a project outside the current directory diff --git a/CLAUDE.md b/CLAUDE.md index 890774414..5e314906f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -134,6 +134,7 @@ The [.claude](.claude) folder contains detailed guides: - **[event-processor.md](.claude/event-processor.md)** - Worker event processing deep-dive (ordering, batching) — companion to `packages/ws-worker/CLAUDE.md` - **[yaml-formats.md](.claude/yaml-formats.md)** - v1 vs v2 project YAML formats: structure, detection logic, and conversion paths +- **[unit-testing-jobs.md](.claude/unit-testing-jobs.md)** - How to compile job for unit-testing Key packages also carry their own `CLAUDE.md` (runtime, engine-multi, ws-worker), auto-loaded when you work in them. diff --git a/integration-tests/cli/CHANGELOG.md b/integration-tests/cli/CHANGELOG.md index 3a00f36b2..f5169ce86 100644 --- a/integration-tests/cli/CHANGELOG.md +++ b/integration-tests/cli/CHANGELOG.md @@ -1,5 +1,13 @@ # @openfn/integration-tests-cli +## 1.0.26 + +### Patch Changes + +- Updated dependencies [ac44559] + - @openfn/project@0.18.0 + - @openfn/lightning-mock@2.4.24 + ## 1.0.25 ### Patch Changes diff --git a/integration-tests/cli/package.json b/integration-tests/cli/package.json index 4bf6b243a..6b5890c57 100644 --- a/integration-tests/cli/package.json +++ b/integration-tests/cli/package.json @@ -1,7 +1,7 @@ { "name": "@openfn/integration-tests-cli", "private": true, - "version": "1.0.25", + "version": "1.0.26", "description": "CLI integration tests", "author": "Open Function Group ", "license": "ISC", diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index 117d78cdb..6d3e237b4 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,5 +1,26 @@ # @openfn/cli +## 1.39.0 + +### Minor Changes + +- ac44559: Improve `openfn compile` for unit-testing job code + + - Add `--exports-only` to strip adaptor operation calls, keeping only exported declarations + - Compile whole projects (or a single workflow by name) to `dist/` as `.mjs` files + - Add `--clean` to remove the output folder before compiling + - Add `--workspace` support (as in `openfn execute` and `openfn project`) + - Support a `dirs.compiled` key in the workspace config (openfn.yaml) to set the output folder + - Watch mode now respects the configured workflows and output directories + +### Patch Changes + +- bec2581: Fix for cleaner output to stdout +- Updated dependencies [ac44559] + - @openfn/compiler@1.3.0 + - @openfn/lexicon@2.4.0 + - @openfn/project@0.18.0 + ## 1.38.5 ### Patch Changes diff --git a/packages/cli/package.json b/packages/cli/package.json index e1c5a7120..ee7991da2 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@openfn/cli", - "version": "1.38.5", + "version": "1.39.0", "description": "CLI devtools for the OpenFn toolchain", "engines": { "node": ">=18", @@ -60,6 +60,7 @@ "@openfn/project": "workspace:^", "@openfn/runtime": "workspace:*", "chalk": "^5.6.2", + "chokidar": "^3.6.0", "dotenv": "^17.3.1", "dotenv-expand": "^12.0.3", "figures": "^5.0.0", diff --git a/packages/cli/src/compile/command.ts b/packages/cli/src/compile/command.ts index b0f9029ac..527b8893e 100644 --- a/packages/cli/src/compile/command.ts +++ b/packages/cli/src/compile/command.ts @@ -1,6 +1,7 @@ import yargs from 'yargs'; import { Opts } from '../options'; import * as o from '../options'; +import * as po from '../projects/options'; import { build, ensure, override } from '../util/command-builders'; export type CompileOptions = Pick< @@ -8,8 +9,10 @@ export type CompileOptions = Pick< | 'adaptors' | 'command' | 'expandAdaptors' + | 'exportsOnly' | 'ignoreImports' | 'expressionPath' + | 'planPath' | 'logJson' | 'log' | 'outputPath' @@ -19,39 +22,46 @@ export type CompileOptions = Pick< | 'useAdaptorsMonorepo' | 'globals' | 'trace' + | 'watch' + | 'workflowName' > & { workflow?: Opts['workflow']; repoDir?: string; + workspace?: string; + clean?: boolean; }; const options = [ o.expandAdaptors, // order important o.adaptors, + override(po.clean, { + description: 'Remove the output folder before compiling', + }), + o.exportsOnly, o.ignoreImports, o.inputPath, o.log, o.logJson, - override(o.outputStdout, { - default: true, - }), + o.outputStdout, o.outputPath, o.repoDir, o.trace, o.useAdaptorsMonorepo, + o.watchFlag, o.workflow, + po.workspace, ]; const compileCommand: yargs.CommandModule = { command: 'compile [path]', describe: - 'Compile an openfn job or workflow and print or save the resulting JavaScript.', + 'Compile an openfn job, workflow, or whole project and print or save the resulting JavaScript.', handler: ensure('compile', options), builder: (yargs) => build(options, yargs) .positional('path', { describe: - 'The path to load the job or workflow from (a .js or .json file or a dir containing a job.js file)', - demandOption: true, + 'Path to a .js expression, .json/.yaml workflow, or a project directory. Omit to compile all workflows in the current project.', }) .example( 'compile foo/job.js', @@ -59,8 +69,37 @@ const compileCommand: yargs.CommandModule = { ) .example( 'compile foo/workflow.json -o foo/workflow-compiled.json', - 'Compiles the workflow at foo/work.json and prints the result to -o foo/workflow-compiled.json' - ), + 'Compiles the workflow and writes to the given path' + ) + .example( + 'compile', + 'Compiles all workflows in the current project and writes JS files to dist/' + ) + .example( + 'compile my-workflow', + 'Compiles a single workflow by name and writes JS files to dist/' + ) + .example( + 'compile my-workflow -O', + 'Compiles a workflow and prints to stdout' + ) + .example( + 'compile foo/job.js --exports-only', + 'Strips adaptor operation calls, keeping only exported declarations' + ) + .example( + 'compile --exports-only', + 'Compiles entire project to dist/ stripping operation calls' + ) + .example( + 'compile --clean', + 'Removes the output folder before compiling the project' + ) + .example( + 'compile foo/job.js --watch', + 'Watches the file and recompiles on every change' + ) + .example('compile --watch', 'Compiles all workflows on change'), }; export default compileCommand; diff --git a/packages/cli/src/compile/compile.ts b/packages/cli/src/compile/compile.ts index 822188702..01c98837b 100644 --- a/packages/cli/src/compile/compile.ts +++ b/packages/cli/src/compile/compile.ts @@ -1,10 +1,15 @@ +import path from 'node:path'; +import fs from 'node:fs/promises'; +import { rimraf } from 'rimraf'; import compile, { preloadAdaptorExports, Options, getExports, + transformers as t, } from '@openfn/compiler'; import { getModulePath, type ExecutionPlan, type Job } from '@openfn/runtime'; import type { SourceMapWithOperations } from '@openfn/lexicon'; +import { Workspace } from '@openfn/project'; import createLogger, { COMPILER, Logger } from '../util/logger'; import abort from '../util/abort'; @@ -52,11 +57,15 @@ const compileJob = async ( jobName?: string ): Promise => { try { + let transformers: any = undefined; const compilerOptions: Options = await loadTransformOptions(opts, log); + if (opts.exportsOnly) { + transformers = [t.exportsOnly, t.lazyState, t.promises, t.addImports]; + } if (jobName) { compilerOptions.name = jobName; } - return compile(job, compilerOptions); + return compile(job, compilerOptions, transformers); } catch (e: any) { abort( log, @@ -69,7 +78,6 @@ const compileJob = async ( } }; -// Find every expression in the job and run the compiler on it const compileWorkflow = async ( plan: ExecutionPlan, opts: CompileOptions, @@ -110,7 +118,6 @@ export const stripVersionSpecifier = (specifier: string) => { return specifier; }; -// Take a module path as provided by the CLI and convert it into a path export const resolveSpecifierPath = async ( pattern: string, repoDir: string | undefined, @@ -119,7 +126,6 @@ export const resolveSpecifierPath = async ( const [specifier, path] = pattern.split('='); if (path) { - // given an explicit path, just load it. log.debug(`Resolved ${specifier} to path: ${path}`); return path; } @@ -131,7 +137,6 @@ export const resolveSpecifierPath = async ( return null; }; -// Mutate the opts object to write export information for the add-imports transformer export const loadTransformOptions = async ( opts: CompileOptions, log: Logger @@ -140,15 +145,13 @@ export const loadTransformOptions = async ( logger: log || createLogger(COMPILER, opts as any), trace: opts.trace, }; - // If an adaptor is passed in, we need to look up its declared exports - // and pass them along to the compiler + if (opts.adaptors?.length && opts.ignoreImports != true) { const adaptorsConfig = []; for (const adaptorInput of opts.adaptors) { let exports; const [specifier] = adaptorInput.split('='); - // Preload exports from a path, optionally logging errors in case of a failure log.debug(`Trying to preload types for ${specifier}`); const path = await resolveSpecifierPath(adaptorInput, opts.repoDir, log); if (path) { @@ -179,3 +182,93 @@ export const loadTransformOptions = async ( return options; }; + +export const compileProject = async ( + opts: CompileOptions, + log: Logger, + workspacePath: string, + workflowFilter?: string +): Promise => { + // validate=false suppresses warnings when workspace config has no extra metadata + const workspace = new Workspace(workspacePath, log as any, false); + const project = await workspace.getCheckedOutProject(); + + if (!project) { + log.error( + 'No project found. Run from a directory containing openfn.yaml, or provide a path.' + ); + process.exit(1); + } + + const wsConfig = workspace.getConfig(); + + const compiledDir = opts.outputStdout + ? null + : path.resolve( + workspacePath, + opts.outputPath ?? wsConfig.dirs?.compiled ?? 'dist' + ); + + if (compiledDir) { + if (opts.clean) { + log.info(`Cleaning ${compiledDir}`); + await rimraf(compiledDir); + } + log.info(`Compiling project to ${compiledDir}`); + } + + let workflows = project.workflows; + if (workflowFilter) { + workflows = workflows.filter( + (wf: any) => wf.id === workflowFilter || wf.name === workflowFilter + ); + if (workflows.length === 0) { + log.error(`Workflow '${workflowFilter}' not found in project.`); + process.exit(1); + } + } + + const outPaths: string[] = []; + + const allSteps = workflows.flatMap((wf: any) => + wf.steps + .filter((step: any) => step.expression) + .map((step: any) => ({ workflow: wf, step })) + ); + + for (const { workflow, step } of allSteps) { + const stepOpts: CompileOptions = { + ...opts, + adaptors: step.adaptor ? [step.adaptor] : opts.adaptors ?? [], + }; + + const { code } = await compileJob( + step.expression, + stepOpts, + log, + step.name ?? step.id + ); + + const stepId = `${workflow.id}/${step.id}`; + + if (opts.exportsOnly && !code.trim()) { + log.debug(` ${stepId} — skipped (empty after stripping)`); + continue; + } + + if (opts.outputStdout) { + log.success(`// ${stepId}\n\n` + code); + } else { + const outPath = path.join(compiledDir!, workflow.id, `${step.id}.mjs`); + await fs.mkdir(path.dirname(outPath), { recursive: true }); + await fs.writeFile(outPath, code); + outPaths.push(outPath); + log.info(`Compiled ${stepId} to ${outPath}`); + } + } + + if (!opts.outputStdout) { + log.success(`Compiled ${outPaths.length} step(s) to ${compiledDir}`); + } + return outPaths; +}; diff --git a/packages/cli/src/compile/handler.ts b/packages/cli/src/compile/handler.ts index 4c75415cc..6c289ad6b 100644 --- a/packages/cli/src/compile/handler.ts +++ b/packages/cli/src/compile/handler.ts @@ -1,16 +1,44 @@ -import { mkdir, writeFile } from 'node:fs/promises'; -import { dirname } from 'node:path'; +import path from 'node:path'; +import { writeFile, mkdir } from 'node:fs/promises'; +import chokidar from 'chokidar'; +import { Workspace } from '@openfn/project'; import type { CompileOptions } from './command'; import type { Logger } from '../util/logger'; -import compile from './compile'; +import compile, { compileProject } from './compile'; import loadPlan from '../util/load-plan'; import assertPath from '../util/assert-path'; -const compileHandler = async (options: CompileOptions, logger: Logger) => { - assertPath(options.path); +// planPath/expressionPath are set by the input-path option when options.path +// looks like a file; a bare workflow name sets neither +const isFileInput = (options: CompileOptions) => + Boolean(options.planPath || options.expressionPath); + +const collectWatchOptions = (options: CompileOptions, logger: Logger) => { + const ignored = ['**/node_modules/**']; - let result; + if (options.expressionPath) { + return { targets: [path.resolve(options.expressionPath)], ignored }; + } + if (options.path && isFileInput(options)) { + return { targets: [path.resolve(options.path)], ignored }; + } + + // Project mode: the output dir must be ignored, or writing compiled files + // would retrigger the watcher + const workspace = new Workspace(options.workspace!, logger as any, false); + const outDir = path.resolve( + options.workspace!, + options.outputPath ?? workspace.getConfig().dirs?.compiled ?? 'dist' + ); + return { + targets: [path.join(workspace.workflowsPath, '**', '*.js')], + ignored: [...ignored, path.join(outDir, '**')], + }; +}; + +const doCompile = async (options: CompileOptions, logger: Logger) => { + let result: string; if (options.expressionPath) { const { code } = await compile(options.expressionPath, options, logger); result = code; @@ -20,13 +48,53 @@ const compileHandler = async (options: CompileOptions, logger: Logger) => { result = JSON.stringify(compiledPlan, null, 2); } - if (options.outputStdout) { + if (options.outputPath) { + await mkdir(path.dirname(options.outputPath), { recursive: true }); + await writeFile(options.outputPath, result); + logger.success(`Compiled to ${options.outputPath}`); + } else { logger.success('Result:\n\n' + result); + } +}; + +const runCompile = async (options: CompileOptions, logger: Logger) => { + if (isFileInput(options)) { + assertPath(options.expressionPath ?? options.planPath); + await doCompile(options, logger); } else { - await mkdir(dirname(options.outputPath!), { recursive: true }); - await writeFile(options.outputPath!, result as string); - logger.success(`Compiled to ${options.outputPath}`); + await compileProject( + options, + logger, + options.workspace!, + options.workflowName + ); } }; +const compileHandler = async (options: CompileOptions, logger: Logger) => { + await runCompile(options, logger); + + if (!options.watch) return; + + const { targets, ignored } = collectWatchOptions(options, logger); + logger.info(`Watching for changes. Ctrl+C to stop.`); + + const watcher = chokidar.watch(targets, { + ignoreInitial: true, + ignored, + }); + + watcher.on('change', async (changedPath: string) => { + logger.info(`${changedPath} changed, recompiling...`); + try { + await runCompile(options, logger); + } catch (e) { + logger.error('Compilation error:', e); + } + }); + + // Keep the process alive + await new Promise(() => {}); +}; + export default compileHandler; diff --git a/packages/cli/src/execute/serialize-output.ts b/packages/cli/src/execute/serialize-output.ts index 175b3b6a6..611c5844c 100644 --- a/packages/cli/src/execute/serialize-output.ts +++ b/packages/cli/src/execute/serialize-output.ts @@ -4,7 +4,7 @@ import { Opts } from '../options'; import { dirname } from 'node:path'; const serializeOutput = async ( - options: Pick, + options: Pick, result: any, logger: Logger ) => { @@ -22,7 +22,12 @@ const serializeOutput = async ( if (options.outputStdout) { logger.success(`Result: `); - logger.always(output); + if (options.log?.default === 'none') { + // special case because if we use logger.always the output won't be seen + console.log(output); + } else { + logger.always(output); + } } else if (options.outputPath) { await mkdir(dirname(options.outputPath), { recursive: true }); diff --git a/packages/cli/src/options.ts b/packages/cli/src/options.ts index 64087498b..557560574 100644 --- a/packages/cli/src/options.ts +++ b/packages/cli/src/options.ts @@ -66,8 +66,10 @@ export type Opts = { statePath?: string; stateStdin?: string; timeout?: number; // ms + exportsOnly?: boolean; trace?: boolean; useAdaptorsMonorepo?: boolean; + watch?: boolean; workflow: string; workflowName?: string; validate?: boolean; @@ -632,6 +634,26 @@ export const validate: CLIOption = { }, }; +export const exportsOnly: CLIOption = { + name: 'exports-only', + yargs: { + boolean: true, + description: + 'Strip adaptor operation calls, exporting only constants and functions', + default: false, + }, +}; + +export const watchFlag: CLIOption = { + name: 'watch', + yargs: { + alias: ['w'], + boolean: true, + description: 'Watch source files and recompile on change', + default: false, + }, +}; + export const workflow: CLIOption = { name: 'workflow', yargs: { diff --git a/packages/cli/test/compile/compile.test.ts b/packages/cli/test/compile/compile.test.ts index 6acdb9bf8..cab8e333b 100644 --- a/packages/cli/test/compile/compile.test.ts +++ b/packages/cli/test/compile/compile.test.ts @@ -1,5 +1,6 @@ import test from 'ava'; import mock from 'mock-fs'; +import fs from 'node:fs/promises'; import path from 'node:path'; import { createMockLogger } from '@openfn/logger'; @@ -7,6 +8,7 @@ import compile, { stripVersionSpecifier, loadTransformOptions, resolveSpecifierPath, + compileProject, } from '../../src/compile/compile'; import { CompileOptions } from '../../src/compile/command'; import { mockFs, resetMockFs } from '../util'; @@ -77,6 +79,156 @@ test.serial('compile from execution plan', async (t) => { t.is((b as Job).expression, expected); }); +test.serial('compile from project', async (t) => { + mockFs({ + '/proj/openfn.yaml': ` +dirs: + workflows: /proj/workflows +`, + '/proj/workflows/wf1/wf1.yaml': ` +id: wf1 +steps: + - id: step-a + expression: "x();" +`, + }); + + const outPaths = await compileProject({} as CompileOptions, mockLog, '/proj'); + + t.is(outPaths.length, 1); + t.true(outPaths[0].endsWith('dist/wf1/step-a.mjs')); + + const code = await fs.readFile(outPaths[0], 'utf-8'); + t.true(code.includes('export default [x()]')); + + mock.restore(); +}); + +test.serial( + 'compile from project, skipping steps left empty by --exports-only', + async (t) => { + mockFs({ + '/proj/openfn.yaml': ` +dirs: + workflows: /proj/workflows +`, + '/proj/workflows/wf1/wf1.yaml': ` +id: wf1 +steps: + - id: step-a + expression: "export const helper = () => {}; fn();" + - id: step-b + expression: "fn();" +`, + }); + + const outPaths = await compileProject( + { exportsOnly: true } as CompileOptions, + mockLog, + '/proj' + ); + + // Only step-a has exportable code — step-b should not be written + t.is(outPaths.length, 1); + t.true(outPaths[0].endsWith('step-a.mjs')); + + mock.restore(); + } +); + +test.serial( + 'compile from project to the dirs.compiled folder in the workspace config', + async (t) => { + mockFs({ + '/proj/openfn.yaml': ` +dirs: + workflows: /proj/workflows + compiled: build +`, + '/proj/workflows/wf1/wf1.yaml': ` +id: wf1 +steps: + - id: step-a + expression: "x();" +`, + }); + + const outPaths = await compileProject( + {} as CompileOptions, + mockLog, + '/proj' + ); + + t.is(outPaths.length, 1); + t.true(outPaths[0].startsWith('/proj/build/')); + + mock.restore(); + } +); + +test.serial( + 'compile from project with --clean, removing the output dir first', + async (t) => { + mockFs({ + '/proj/openfn.yaml': ` +dirs: + workflows: /proj/workflows +`, + '/proj/workflows/wf1/wf1.yaml': ` +id: wf1 +steps: + - id: step-a + expression: "x();" +`, + // A stale file from a previous compile + '/proj/dist/old-workflow/stale.mjs': 'export default [];', + }); + + // Without --clean, the stale file survives + await compileProject({} as CompileOptions, mockLog, '/proj'); + await t.notThrowsAsync(() => + fs.access('/proj/dist/old-workflow/stale.mjs') + ); + + // With --clean, it is removed + const outPaths = await compileProject( + { clean: true } as CompileOptions, + mockLog, + '/proj' + ); + t.is(outPaths.length, 1); + await t.throwsAsync(() => fs.access('/proj/dist/old-workflow/stale.mjs')); + + mock.restore(); + } +); + +test.serial('compile from project to outputPath', async (t) => { + mockFs({ + '/proj/openfn.yaml': ` +dirs: + workflows: /proj/workflows +`, + '/proj/workflows/wf1/wf1.yaml': ` +id: wf1 +steps: + - id: step-a + expression: "x();" +`, + }); + + const outPaths = await compileProject( + { outputPath: '/out' } as CompileOptions, + mockLog, + '/proj' + ); + + t.is(outPaths.length, 1); + t.true(outPaths[0].startsWith('/out/')); + + mock.restore(); +}); + test.serial('throw an AbortError if a job is uncompilable', async (t) => { const job = 'a b'; @@ -342,3 +494,12 @@ test.serial('loadTransformOptions: ignore some imports', async (t) => { }); // TODO test exception if the module can't be found + +test.serial( + 'loadTransformOptions: --exports-only does not affect other options', + async (t) => { + const opts = { exportsOnly: true } as CompileOptions; + const result = await loadTransformOptions(opts, mockLog); + t.falsy(result['add-imports']); + } +); diff --git a/packages/cli/test/compile/handler.test.ts b/packages/cli/test/compile/handler.test.ts new file mode 100644 index 000000000..24ec95d2e --- /dev/null +++ b/packages/cli/test/compile/handler.test.ts @@ -0,0 +1,94 @@ +import test from 'ava'; +import fs from 'node:fs/promises'; +import { createMockLogger } from '@openfn/logger'; + +import compileHandler from '../../src/compile/handler'; +import { CompileOptions } from '../../src/compile/command'; +import { mockFs, resetMockFs } from '../util'; + +const logger = createMockLogger(); + +test.afterEach(() => { + logger._reset(); +}); + +test.after(resetMockFs); + +test.serial('compile an expression file to stdout', async (t) => { + mockFs({ + '/job.js': 'x();', + }); + + const options = { expressionPath: '/job.js' } as CompileOptions; + await compileHandler(options, logger); + + t.truthy(logger._find('success', /export default \[x\(\)\];/)); +}); + +test.serial('compile an expression file to outputPath', async (t) => { + mockFs({ + '/job.js': 'x();', + }); + + const options = { + expressionPath: '/job.js', + outputPath: '/out/job.js', + } as CompileOptions; + await compileHandler(options, logger); + + const code = await fs.readFile('/out/job.js', 'utf-8'); + t.is(code, 'export default [x()];'); +}); + +test.serial('compile a workflow file to outputPath', async (t) => { + mockFs({ + '/wf.json': JSON.stringify({ + workflow: { + steps: [{ id: 'a', expression: 'x()' }], + }, + }), + }); + + const options = { + planPath: '/wf.json', + outputPath: '/out/wf.json', + } as CompileOptions; + await compileHandler(options, logger); + + const plan = JSON.parse(await fs.readFile('/out/wf.json', 'utf-8')); + t.is(plan.workflow.steps[0].expression, 'export default [x()];'); +}); + +test.serial( + 'compile the checked-out project when no path is given', + async (t) => { + mockFs({ + '/proj/openfn.yaml': ` +dirs: + workflows: /proj/workflows +`, + '/proj/workflows/wf1/wf1.yaml': ` +id: wf1 +steps: + - id: step-a + expression: "x();" +`, + }); + + const options = { workspace: '/proj' } as CompileOptions; + await compileHandler(options, logger); + + const code = await fs.readFile('/proj/dist/wf1/step-a.mjs', 'utf-8'); + t.true(code.includes('export default [x()]')); + } +); + +test.serial('throw if the expression file does not exist', async (t) => { + mockFs({}); + + const options = { expressionPath: '/missing.js' } as CompileOptions; + + await t.throwsAsync(() => compileHandler(options, logger), { + message: /failed to compile/i, + }); +}); diff --git a/packages/cli/test/compile/options.test.ts b/packages/cli/test/compile/options.test.ts index a43b76b6e..e6a557a0c 100644 --- a/packages/cli/test/compile/options.test.ts +++ b/packages/cli/test/compile/options.test.ts @@ -15,7 +15,7 @@ test('correct default options', (t) => { t.is(options.expandAdaptors, true); t.is(options.expressionPath, 'job.js'); t.falsy(options.logJson); // TODO this is undefined right now - t.is(options.outputStdout, true); + t.is(options.outputStdout, false); t.is(options.path, 'job.js'); t.falsy(options.useAdaptorsMonorepo); }); @@ -91,3 +91,40 @@ test('disable some imports', (t) => { t.is(a, 'jam'); t.is(b, 'jar'); }); + +test('clean is disabled by default', (t) => { + const options = parse('compile'); + t.false(options.clean); +}); + +test('enable clean', (t) => { + const options = parse('compile --clean'); + t.true(options.clean); +}); + +test('workspace defaults to cwd', (t) => { + const options = parse('compile'); + t.is(options.workspace, process.cwd()); +}); + +test('set a workspace path', (t) => { + const options = parse('compile --workspace /tmp/proj'); + t.is(options.workspace, '/tmp/proj'); +}); + +test('a path with a file extension maps to a file input', (t) => { + const options = parse('compile job.js'); + t.is(options.expressionPath, 'job.js'); + t.falsy(options.workflowName); + + const wfOptions = parse('compile workflow.json'); + t.is(wfOptions.planPath, 'workflow.json'); + t.falsy(wfOptions.workflowName); +}); + +test('a path without a file extension maps to a workflow name', (t) => { + const options = parse('compile my-workflow'); + t.is(options.workflowName, 'my-workflow'); + t.falsy(options.expressionPath); + t.falsy(options.planPath); +}); diff --git a/packages/compiler/CHANGELOG.md b/packages/compiler/CHANGELOG.md index edc11af03..82d1fd546 100644 --- a/packages/compiler/CHANGELOG.md +++ b/packages/compiler/CHANGELOG.md @@ -1,5 +1,23 @@ # @openfn/compiler +## 1.3.0 + +### Minor Changes + +- ac44559: Improve `openfn compile` for unit-testing job code + + - Add `--exports-only` to strip adaptor operation calls, keeping only exported declarations + - Compile whole projects (or a single workflow by name) to `dist/` as `.mjs` files + - Add `--clean` to remove the output folder before compiling + - Add `--workspace` support (as in `openfn execute` and `openfn project`) + - Support a `dirs.compiled` key in the workspace config (openfn.yaml) to set the output folder + - Watch mode now respects the configured workflows and output directories + +### Patch Changes + +- Updated dependencies [ac44559] + - @openfn/lexicon@2.4.0 + ## 1.2.5 ### Patch Changes diff --git a/packages/compiler/package.json b/packages/compiler/package.json index 21d60dc03..e7e113751 100644 --- a/packages/compiler/package.json +++ b/packages/compiler/package.json @@ -1,6 +1,6 @@ { "name": "@openfn/compiler", - "version": "1.2.5", + "version": "1.3.0", "description": "Compiler and language tooling for openfn jobs.", "author": "Open Function Group ", "license": "ISC", diff --git a/packages/compiler/src/compile.ts b/packages/compiler/src/compile.ts index 297682cbe..9899c08c3 100644 --- a/packages/compiler/src/compile.ts +++ b/packages/compiler/src/compile.ts @@ -2,7 +2,7 @@ import { print } from 'recast'; import createLogger, { Logger } from '@openfn/logger'; import parse from './parse'; -import transform, { TransformOptions } from './transform'; +import transform, { TransformOptions, Transformer } from './transform'; import { isPath, loadFile } from './util'; import type { SourceMapWithOperations } from '@openfn/lexicon'; @@ -23,7 +23,8 @@ export type Options = TransformOptions & { export default function compile( pathOrSource: string, - options: Options = {} + options: Options = {}, + transformers?: Transformer[] ): { code: string; map?: SourceMapWithOperations; @@ -42,7 +43,7 @@ export default function compile( const name = options.name ?? 'src'; const trace = options.trace; const ast = parse(source, { logger, name, trace }); - const transformedAst = transform(ast, undefined, options); + const transformedAst = transform(ast, transformers, options); const { code, map } = print(transformedAst, { sourceMapName: `${name}.map.js`, diff --git a/packages/compiler/src/index.ts b/packages/compiler/src/index.ts index 401388960..00b94ed5a 100644 --- a/packages/compiler/src/index.ts +++ b/packages/compiler/src/index.ts @@ -2,6 +2,7 @@ import compile from './compile'; export { default as getExports } from './get-exports'; export * from './util'; +export * as transformers from './transforms/index'; export type { TransformOptions } from './transform'; export type { Options } from './compile'; export default compile; diff --git a/packages/compiler/src/transform.ts b/packages/compiler/src/transform.ts index 0e3245960..367b9fa53 100644 --- a/packages/compiler/src/transform.ts +++ b/packages/compiler/src/transform.ts @@ -15,6 +15,7 @@ import { heap } from './util'; export type TransformerName = | 'add-imports' | 'ensure-exports' + | 'exports-only' | 'top-level-operations' | 'test' | 'lazy-state'; @@ -38,6 +39,7 @@ export type TransformOptions = { ['add-imports']?: AddImportsOptions | boolean; ['ensure-exports']?: boolean; + ['exports-only']?: boolean; ['top-level-operations']?: TopLevelOpsOptions | boolean; ['test']?: any; ['lazy-state']?: any; diff --git a/packages/compiler/src/transforms/exports-only.ts b/packages/compiler/src/transforms/exports-only.ts new file mode 100644 index 000000000..c07e81024 --- /dev/null +++ b/packages/compiler/src/transforms/exports-only.ts @@ -0,0 +1,63 @@ +/* + * Strips all non-exported top-level code (operations, export default, bare declarations). + */ + +import { namedTypes as n } from 'ast-types'; +import type { NodePath } from 'ast-types/lib/node-path'; +import type { Transformer } from '../transform'; + +// Names referenced by bare export lists, eg export { x, y } +const findExportListNames = (body: any[]) => { + const names = new Set(); + for (const node of body) { + if ( + n.ExportNamedDeclaration.check(node) && + !node.declaration && + !node.source + ) { + for (const spec of node.specifiers ?? []) { + if (n.Identifier.check(spec.local)) { + names.add(spec.local.name); + } + } + } + } + return names; +}; + +const declaresName = (node: any, names: Set) => { + if (n.FunctionDeclaration.check(node) || n.ClassDeclaration.check(node)) { + return Boolean(node.id && names.has(node.id.name)); + } + if (n.VariableDeclaration.check(node)) { + return node.declarations.some( + (d) => + n.VariableDeclarator.check(d) && + n.Identifier.check(d.id) && + names.has(d.id.name) + ); + } + return false; +}; + +function visitor(programPath: NodePath) { + const { body } = programPath.node; + const exportListNames = findExportListNames(body); + + programPath.node.body = body.filter( + (node) => + n.ImportDeclaration.check(node) || + n.ExportNamedDeclaration.check(node) || + n.ExportAllDeclaration.check(node) || + declaresName(node, exportListNames) + ) as any; + + return true; // abort further traversal +} + +export default { + id: 'exports-only', + types: ['Program'], + order: 0, + visitor, +} as unknown as Transformer; diff --git a/packages/compiler/src/transforms/index.ts b/packages/compiler/src/transforms/index.ts new file mode 100644 index 000000000..765e80e99 --- /dev/null +++ b/packages/compiler/src/transforms/index.ts @@ -0,0 +1,9 @@ +export { default as addImports } from './add-imports'; +export { default as ensureExports } from './ensure-exports'; +export { default as exportsOnly } from './exports-only'; +export { default as lazyState } from './lazy-state'; +export { default as promises } from './promises'; +export { default as topLevelOps } from './top-level-operations'; + +export type { AddImportsOptions } from './add-imports'; +export type { TopLevelOpsOptions } from './top-level-operations'; diff --git a/packages/compiler/test/compile.test.ts b/packages/compiler/test/compile.test.ts index 614e26344..529f3889f 100644 --- a/packages/compiler/test/compile.test.ts +++ b/packages/compiler/test/compile.test.ts @@ -2,6 +2,7 @@ import test from 'ava'; import fs from 'node:fs/promises'; import path from 'node:path'; import compile from '../src/compile'; +import exportsOnly from '../src/transforms/exports-only'; // Not doing deep testing on this because recast does the heavy lifting // This is just to ensure the map is actually generated @@ -278,3 +279,24 @@ test('respect ignore list when exports not provided', (t) => { const { code: result } = compile(source, options); t.is(result, expected); }); + +const exportsOnlyOpts = { + 'exports-only': true, + 'ensure-exports': false, + 'top-level-operations': false, +} as const; + +test('only run transformers that are passed in', (t) => { + const source = [ + 'export const formatDate = (d) => d.toISOString();', + 'get("/api");', + 'fn(state => ({ ...state, date: formatDate(state.data.date) }));', + ].join('\n'); + + const { code: result } = compile(source, exportsOnlyOpts, [exportsOnly]); + + t.true(result.includes('export const formatDate')); + t.false(result.includes('get(')); + t.false(result.includes('fn(state')); + t.false(result.includes('export default []')); +}); diff --git a/packages/compiler/test/transforms/exports-only.test.ts b/packages/compiler/test/transforms/exports-only.test.ts new file mode 100644 index 000000000..49302cddb --- /dev/null +++ b/packages/compiler/test/transforms/exports-only.test.ts @@ -0,0 +1,214 @@ +import test from 'ava'; +import { print } from 'recast'; +import parse from '../../src/parse'; +import transform from '../../src/transform'; +import visitors from '../../src/transforms/exports-only'; + +const enabled = { 'exports-only': true }; +const disabled = { 'exports-only': false }; + +test('strips a non-exported value', (t) => { + const before = `const x = 42; +fn();`; + const ast = parse(before); + const transformed = transform(ast, [visitors], enabled); + const after = print(transformed).code; + + t.is(after, ''); +}); + +test('is a no-op if exports-only is false', (t) => { + const before = `const x = 42; +fn();`; + const ast = parse(before); + const transformed = transform(ast, [visitors], disabled); + const after = print(transformed).code; + + t.is(after, before); +}); + +test('strips operation calls', (t) => { + const before = `get(); +fn();`; + const ast = parse(before); + const transformed = transform(ast, [visitors], enabled); + const after = print(transformed).code; + + t.is(after, ''); +}); + +test('strips export default []', (t) => { + const before = `fn(); +export default [];`; + const ast = parse(before); + const transformed = transform(ast, [visitors], enabled); + const after = print(transformed).code; + + t.is(after, ''); +}); + +test('keeps import declarations', (t) => { + const before = `import { get } from '@openfn/language-http'; +fn();`; + const ast = parse(before); + const transformed = transform(ast, [visitors], enabled); + const after = print(transformed).code; + + t.is(after, `import { get } from '@openfn/language-http';`); +}); + +test('keeps named export declarations', (t) => { + const before = `export const helper = 42; +fn();`; + const ast = parse(before); + const transformed = transform(ast, [visitors], enabled); + const after = print(transformed).code; + + t.is(after, `export const helper = 42;`); +}); + +test('keeps exported function declarations', (t) => { + const before = `export function formatDate() { + return 1; +} +fn();`; + const ast = parse(before); + const transformed = transform(ast, [visitors], enabled); + const after = print(transformed).code; + + t.is( + after, + `export function formatDate() { + return 1; +}` + ); +}); + +test('drops non-exported declarations that no export depends on', (t) => { + const before = `const unused = 42; +export function greet() { + return 'hi'; +} +fn();`; + const ast = parse(before); + const transformed = transform(ast, [visitors], enabled); + const after = print(transformed).code; + + t.is( + after, + `export function greet() { + return 'hi'; +}` + ); +}); + +test('keeps imports alongside named exports', (t) => { + const before = `import { dateFns } from '@openfn/language-dhis2'; +export const formatDate = 42; +fn();`; + const ast = parse(before); + const transformed = transform(ast, [visitors], enabled); + const after = print(transformed).code; + + t.is( + after, + `import { dateFns } from '@openfn/language-dhis2'; +export const formatDate = 42;` + ); +}); + +test('keeps declarations referenced by an export list', (t) => { + const before = `const x = 1; +export { x }; +fn();`; + const ast = parse(before); + const transformed = transform(ast, [visitors], enabled); + const after = print(transformed).code; + + t.is( + after, + `const x = 1; +export { x };` + ); +}); + +test('keeps a function referenced by an aliased export list', (t) => { + const before = `function formatDate() { + return 1; +} +export { formatDate as format }; +fn();`; + const ast = parse(before); + const transformed = transform(ast, [visitors], enabled); + const after = print(transformed).code; + + t.is( + after, + `function formatDate() { + return 1; +} +export { formatDate as format };` + ); +}); + +test('keeps re-exports from another module', (t) => { + const before = `export { helper } from './helpers'; +fn();`; + const ast = parse(before); + const transformed = transform(ast, [visitors], enabled); + const after = print(transformed).code; + + t.is(after, `export { helper } from './helpers';`); +}); + +test('keeps export * from another module', (t) => { + const before = `export * from './helpers'; +fn();`; + const ast = parse(before); + const transformed = transform(ast, [visitors], enabled); + const after = print(transformed).code; + + t.is(after, `export * from './helpers';`); +}); + +test('handles a file with only operations (no exports)', (t) => { + const before = `fn(); +get();`; + const ast = parse(before); + const transformed = transform(ast, [visitors], enabled); + const after = print(transformed).code; + + t.is(after, ''); +}); + +test('handles an empty file', (t) => { + const before = ''; + const ast = parse(before); + const transformed = transform(ast, [visitors], enabled); + const after = print(transformed).code; + + t.is(after, ''); +}); + +test('handles multiple exports without operations', (t) => { + const before = `export const a = 42; +export const b = 42; +export function c() { + return 1; +}`; + const ast = parse(before); + const transformed = transform(ast, [visitors], enabled); + const after = print(transformed).code; + + t.is(after, before); +}); + +test('does not remove export default when exports-only is disabled', (t) => { + const before = `fn(); +export default [];`; + const ast = parse(before); + const transformed = transform(ast, [visitors], disabled); + const after = print(transformed).code; + + t.is(after, before); +}); diff --git a/packages/engine-multi/CHANGELOG.md b/packages/engine-multi/CHANGELOG.md index fd5fb6940..d23b49653 100644 --- a/packages/engine-multi/CHANGELOG.md +++ b/packages/engine-multi/CHANGELOG.md @@ -1,5 +1,13 @@ # engine-multi +## 1.12.3 + +### Patch Changes + +- Updated dependencies [ac44559] + - @openfn/compiler@1.3.0 + - @openfn/lexicon@2.4.0 + ## 1.12.2 ### Patch Changes diff --git a/packages/engine-multi/package.json b/packages/engine-multi/package.json index 54ea77cac..b50878cba 100644 --- a/packages/engine-multi/package.json +++ b/packages/engine-multi/package.json @@ -1,6 +1,6 @@ { "name": "@openfn/engine-multi", - "version": "1.12.2", + "version": "1.12.3", "description": "Multi-process runtime engine", "main": "dist/index.js", "type": "module", diff --git a/packages/lexicon/CHANGELOG.md b/packages/lexicon/CHANGELOG.md index 85715c9ec..491b610ce 100644 --- a/packages/lexicon/CHANGELOG.md +++ b/packages/lexicon/CHANGELOG.md @@ -1,5 +1,18 @@ # lexicon +## 2.4.0 + +### Minor Changes + +- ac44559: Improve `openfn compile` for unit-testing job code + + - Add `--exports-only` to strip adaptor operation calls, keeping only exported declarations + - Compile whole projects (or a single workflow by name) to `dist/` as `.mjs` files + - Add `--clean` to remove the output folder before compiling + - Add `--workspace` support (as in `openfn execute` and `openfn project`) + - Support a `dirs.compiled` key in the workspace config (openfn.yaml) to set the output folder + - Watch mode now respects the configured workflows and output directories + ## 2.3.0 ### Minor Changes diff --git a/packages/lexicon/core.d.ts b/packages/lexicon/core.d.ts index 1158ffb69..bdcfaaf68 100644 --- a/packages/lexicon/core.d.ts +++ b/packages/lexicon/core.d.ts @@ -147,6 +147,7 @@ export interface WorkspaceConfig { dirs: { workflows: string; projects: string; + compiled?: string; }; formats: { openfn?: FileFormats; diff --git a/packages/lexicon/package.json b/packages/lexicon/package.json index 7ea4cc990..51a8e9fc6 100644 --- a/packages/lexicon/package.json +++ b/packages/lexicon/package.json @@ -1,6 +1,6 @@ { "name": "@openfn/lexicon", - "version": "2.3.0", + "version": "2.4.0", "description": "Central repo of names and type definitions", "author": "Open Function Group ", "license": "ISC", diff --git a/packages/lightning-mock/CHANGELOG.md b/packages/lightning-mock/CHANGELOG.md index f865607cc..eec62d60b 100644 --- a/packages/lightning-mock/CHANGELOG.md +++ b/packages/lightning-mock/CHANGELOG.md @@ -1,5 +1,14 @@ # @openfn/lightning-mock +## 2.4.24 + +### Patch Changes + +- Updated dependencies [ac44559] + - @openfn/lexicon@2.4.0 + - @openfn/project@0.18.0 + - @openfn/engine-multi@1.12.3 + ## 2.4.23 ### Patch Changes diff --git a/packages/lightning-mock/package.json b/packages/lightning-mock/package.json index 5451acce4..4bcbaf3cc 100644 --- a/packages/lightning-mock/package.json +++ b/packages/lightning-mock/package.json @@ -1,6 +1,6 @@ { "name": "@openfn/lightning-mock", - "version": "2.4.23", + "version": "2.4.24", "private": true, "description": "A mock Lightning server", "main": "dist/index.js", diff --git a/packages/project/CHANGELOG.md b/packages/project/CHANGELOG.md index 09cadf75d..d8588ef1e 100644 --- a/packages/project/CHANGELOG.md +++ b/packages/project/CHANGELOG.md @@ -1,5 +1,23 @@ # @openfn/project +## 0.18.0 + +### Minor Changes + +- ac44559: Improve `openfn compile` for unit-testing job code + + - Add `--exports-only` to strip adaptor operation calls, keeping only exported declarations + - Compile whole projects (or a single workflow by name) to `dist/` as `.mjs` files + - Add `--clean` to remove the output folder before compiling + - Add `--workspace` support (as in `openfn execute` and `openfn project`) + - Support a `dirs.compiled` key in the workspace config (openfn.yaml) to set the output folder + - Watch mode now respects the configured workflows and output directories + +### Patch Changes + +- Updated dependencies [ac44559] + - @openfn/lexicon@2.4.0 + ## 0.17.2 ### Patch Changes diff --git a/packages/project/package.json b/packages/project/package.json index 1590c10a2..cb2564f8a 100644 --- a/packages/project/package.json +++ b/packages/project/package.json @@ -1,6 +1,6 @@ { "name": "@openfn/project", - "version": "0.17.2", + "version": "0.18.0", "description": "Read, serialize, replicate and sync OpenFn projects", "scripts": { "test": "pnpm ava", diff --git a/packages/project/src/util/config.ts b/packages/project/src/util/config.ts index 17bd59ecc..60ed24ec0 100644 --- a/packages/project/src/util/config.ts +++ b/packages/project/src/util/config.ts @@ -13,6 +13,7 @@ export const buildConfig = (config: Partial = {}) => ({ dirs: { projects: config.dirs?.projects ?? '.projects', workflows: config.dirs?.workflows ?? 'workflows', + ...(config.dirs?.compiled && { compiled: config.dirs.compiled }), }, formats: { openfn: config.formats?.openfn ?? 'yaml', diff --git a/packages/ws-worker/CHANGELOG.md b/packages/ws-worker/CHANGELOG.md index a3de076a9..4bed703a9 100644 --- a/packages/ws-worker/CHANGELOG.md +++ b/packages/ws-worker/CHANGELOG.md @@ -1,5 +1,13 @@ # ws-worker +## 1.27.3 + +### Patch Changes + +- Updated dependencies [ac44559] + - @openfn/lexicon@2.4.0 + - @openfn/engine-multi@1.12.3 + ## 1.27.2 ### Patch Changes diff --git a/packages/ws-worker/package.json b/packages/ws-worker/package.json index 3a00f4ea9..84b3198aa 100644 --- a/packages/ws-worker/package.json +++ b/packages/ws-worker/package.json @@ -1,6 +1,6 @@ { "name": "@openfn/ws-worker", - "version": "1.27.2", + "version": "1.27.3", "description": "A Websocket Worker to connect Lightning to a Runtime Engine", "main": "dist/index.js", "type": "module", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 64b8abe06..48b884fb8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -213,6 +213,9 @@ importers: chalk: specifier: ^5.6.2 version: 5.6.2 + chokidar: + specifier: ^3.6.0 + version: 3.6.0 dotenv: specifier: ^17.3.1 version: 17.3.1