Openfn compile for unit tests#1443
Conversation
- Add --test flag: compiles job expressions for unit testing, writing output to tests/ by default (reads dirs.tests from openfn.yaml) - Add --strip (default on with --test): tree-shakes compiled output, keeping only export const/function declarations and their transitive dependencies; use --no-strip to keep all compiled code - Add --watch flag: watches source files and recompiles on change (uses chokidar) - Strip mode removes injected _defer import when operations are stripped - Skip writing files with no exportable code after stripping; log when skipping - Auto-clean stale step files in tests/ that were skipped in the current run without touching user-added files at other paths - Fix --test --no-strip skipping pure-operation jobs (hasExportableCode guard now only applies when strip is active) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- remove export default [] from strip mode output (not needed for unit testing) - add --no-strip flag to keep full compiled output including operations - remove --strip standalone flag (stripping is always on with --test by default) - auto-derive output path for single-file --test using tests/ dir - skip writing files with no exportable code in strip mode - auto-clean stale step files in tests/ after project-wide strip runs - fix --test --no-strip skipping pure-operation jobs - update help examples and option descriptions Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
@mtuchi I'm not liking the When you do When you do When you do I'd also suggest that So drop the |
…mpile - Drop --test and --strip/--no-strip flags from openfn compile - Add --exports-only flag (opt-in) to strip operation calls, keeping only exported constants and functions for unit testing - openfn compile (no args) now compiles the whole project to compiled/ by default - openfn compile <workflow-name> looks up a workflow by name/id and compiles it - Extract stripping logic into a new exports-only transformer (order: 0) that runs before all others; remove strip option from top-level-operations - Update unit-testing-jobs.md to document the new workflow Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
QA walk through video for @hunterachieng |
|
|
||
| export type CompiledJob = { code: string; map?: SourceMapWithOperations }; | ||
|
|
||
| export const hasExportableCode = (code: string): boolean => |
There was a problem hiding this comment.
what is this here for?
| (step as any).name ?? step.id | ||
| ); | ||
|
|
||
| if (opts.exportsOnly && !hasExportableCode(code)) { |
There was a problem hiding this comment.
The compiler should decide whether there's any exportable code - not the CLI.
The CLI could choose to not write empty files, that would be fine. But we can't have it do compiler stuff.
|
|
||
| for (const workflow of workflows) { | ||
| for (const step of workflow.steps) { | ||
| const expression = (step as any).expression; |
There was a problem hiding this comment.
typing is very messy here. Can we continue if there's no expression in step, and otherwise cast step properly?
or, if we have to cast to any, let's just do it once at the top of the loop. But I don;t think that should be neccessary
| (step as any).adaptor ?? (step as any).adaptors?.[0]; | ||
| const stepOpts: CompileOptions = { | ||
| ...opts, | ||
| adaptors: adaptor ? [adaptor] : opts.adaptors ?? [], |
There was a problem hiding this comment.
This is very complex! Can we simplify?
| const stalePath = compiledDir | ||
| ? path.join(compiledDir, workflow.id, `${step.id}.js`) | ||
| : null; | ||
| log.info( |
There was a problem hiding this comment.
This is a debug log at most
| watcher.on('change', async (changedPath: string) => { | ||
| logger.info(`${changedPath} changed, recompiling...`); | ||
| try { | ||
| if (options.workflowName) { |
There was a problem hiding this comment.
This looks like a duplication of the first half of the function?
| const watchTargets = collectWatchTargets(options); | ||
| logger.info(`Watching for changes. Ctrl+C to stop.`); | ||
|
|
||
| const watcher = chokidar.watch(watchTargets, { |
There was a problem hiding this comment.
nodejs has a --watch argument now. I wonder if you could just that, alongside with --watch-path in the scripts to get the same effect more easily
| (node) => | ||
| n.ImportDeclaration.check(node) || | ||
| n.ExportNamedDeclaration.check(node) || | ||
| needed.has(node as n.Statement) |
There was a problem hiding this comment.
I don't understand this needed bit - what is it trying to do?
I think this should be really simple: accept import and name export statements, and discard the rest.
If we're trying to keep non exported constants and functions and dependencies and stuff, well that's out of scope and more work than I was expecting. Is there a specific use case for that you're looking at?
| import { namedTypes as n, namedTypes } from 'ast-types'; | ||
| import type { NodePath } from 'ast-types/lib/node-path'; | ||
| import type { Transformer } from '../transform'; | ||
| // Note that the validator should complain if it see anything other than export default [] |
There was a problem hiding this comment.
Why are there diffs in this file? This should be unchanged now right?
|
|
||
| // --- exports-only transformer --- | ||
|
|
||
| test('is a no-op when options is not true', (t) => { |
There was a problem hiding this comment.
I would prefer if these tests tests source code before and after
So like
const before = `export const x = () => {}; fn()'`
const after = `fn()`
So much easier to read than the ASTs, and since these test aren't particularly intricate I think it'll work better
Short Description
Update
openfn compileto compiles workflows job expressions to standard JavaScript, writing output tocompiled/by default.Fixes #1424
Implementation Details
openfn compilenow compiles all workflows in the project tocompiled/by default (no flag needed)openfn compile <workflow-name>looks up a workflow by name in the project and compiles it tocompiled/use -Ofor stdout or-o <dir>to redirect outputopenfn compile <file.js>and openfn compile<workflow.json>retain their existing behaviour (stdout by default)Added new exports-only transformer (
src/transforms/exports-only.ts) with a single responsibility: keep imports, named export declarations, and their transitive non-exported dependencies; drop everything else (operations, export default, bare declarations). Runs at order: 0 (beforeall other transformers). Is a no-op unless explicitly enabled.
Added
--exports-onlyflag: strips adaptor operation calls and keeps only exported declarations — intended for unit testing job codeWhen
--exports-onlyis active the CLI setsexports-only: trueand explicitly disablesensure-exportsand top-level-operations, so the output is clean module code with no emptyexport default [].QA Notes
Setup
Check out the
1424-unit-testsbranch and installopenfnx:Verify the correct version is installed —
openfnx --versionshould outputbranch/1424-unit-tests.Testing
openfn compileNavigate to a local OpenFn project containing an
openfn.yamlfile and test the newopenfn compilecommand. Available options:AI Usage
Please disclose whether you've used AI anywhere in this PR (it's cool, we just
want to know!):
You can read more details in our
Responsible AI Policy
Release branch checklist
Delete this section if this is not a release PR.
If this IS a release branch:
pnpm changeset versionfrom root to bump versionspnpm installpnpm changeset tagto generate tagsgit push --tagsTags may need updating if commits come in after the tags are first generated.