From 3910c5ab8e47e1892c6d2894639862eb66241441 Mon Sep 17 00:00:00 2001 From: Joe Clark Date: Thu, 21 Mar 2024 12:57:14 +0000 Subject: [PATCH] compiler: initial support for lazy state operator --- .changeset/nice-kids-itch.md | 5 ++ packages/compiler/src/transform.ts | 7 +- .../compiler/src/transforms/lazy-state.ts | 46 +++++++++++ packages/compiler/test/compile.test.ts | 8 ++ .../test/transforms/lazy-state.test.ts | 76 +++++++++++++++++++ 5 files changed, 140 insertions(+), 2 deletions(-) create mode 100644 .changeset/nice-kids-itch.md create mode 100644 packages/compiler/src/transforms/lazy-state.ts create mode 100644 packages/compiler/test/transforms/lazy-state.test.ts diff --git a/.changeset/nice-kids-itch.md b/.changeset/nice-kids-itch.md new file mode 100644 index 000000000..025f95e70 --- /dev/null +++ b/.changeset/nice-kids-itch.md @@ -0,0 +1,5 @@ +--- +'@openfn/compiler': minor +--- + +Basic support for lazy state ($) operator diff --git a/packages/compiler/src/transform.ts b/packages/compiler/src/transform.ts index 3a5c8ca98..954fcddde 100644 --- a/packages/compiler/src/transform.ts +++ b/packages/compiler/src/transform.ts @@ -5,6 +5,7 @@ import createLogger, { Logger } from '@openfn/logger'; import addImports, { AddImportsOptions } from './transforms/add-imports'; import ensureExports from './transforms/ensure-exports'; +import lazyState from './transforms/lazy-state'; import topLevelOps, { TopLevelOpsOptions, } from './transforms/top-level-operations'; @@ -13,7 +14,8 @@ export type TransformerName = | 'add-imports' | 'ensure-exports' | 'top-level-operations' - | 'test'; + | 'test' + | 'lazy-state'; type TransformFunction = ( path: NodePath, @@ -36,6 +38,7 @@ export type TransformOptions = { ['ensure-exports']?: boolean; ['top-level-operations']?: TopLevelOpsOptions | boolean; ['test']?: any; + ['lazy-state']?: any; }; const defaultLogger = createLogger(); @@ -46,7 +49,7 @@ export default function transform( options: TransformOptions = {} ) { if (!transformers) { - transformers = [ensureExports, topLevelOps, addImports] as Transformer[]; + transformers = [lazyState, ensureExports, topLevelOps, addImports] as Transformer[]; } const logger = options.logger || defaultLogger; const transformerIndex = indexTransformers(transformers, options); diff --git a/packages/compiler/src/transforms/lazy-state.ts b/packages/compiler/src/transforms/lazy-state.ts new file mode 100644 index 000000000..deb552edd --- /dev/null +++ b/packages/compiler/src/transforms/lazy-state.ts @@ -0,0 +1,46 @@ +/* + * Convert $.a.b.c references into (state) => state.a.b.c + * Should this only run at top level? + * Ideally it would run on all arguments to operations - but we probably don't really know what an operation is + * So for now, first pass, it's only top level. + * (alternatively I guess it just dumbly converts everything and if it breaks, it breaks) + * + * TODO (maybe): + * - only convert $-expressions which are arguments to operations (needs type defs) + * - warn if converting a non-top-level $-expression + * - if not top level, convert to state.a.b.c (ie don't wrap the function) + */ +import { builders as b, namedTypes } from 'ast-types'; +import type { NodePath } from 'ast-types/lib/node-path'; +import type { Transformer } from '../transform'; + +function visitor(path: NodePath) { + let first = path.node.object; + while(first.hasOwnProperty('object')) { + first = (first as namedTypes.MemberExpression).object; + } + + let firstIdentifer = first as namedTypes.Identifier; + + if (first && firstIdentifer.name === "$") { + // rename $ to state + firstIdentifer.name = "state"; + + // Now nest the whole thing in an arrow + const params = b.identifier('state') + const arrow = b.arrowFunctionExpression( + [params], + path.node + ) + path.replace(arrow) + } + + // Stop parsing this member expression + return; +} + +export default { + id: 'lazy-state', + types: ['MemberExpression'], + visitor, +} as Transformer; diff --git a/packages/compiler/test/compile.test.ts b/packages/compiler/test/compile.test.ts index fc78d5ef5..b1d8d2800 100644 --- a/packages/compiler/test/compile.test.ts +++ b/packages/compiler/test/compile.test.ts @@ -114,3 +114,11 @@ test('twitter example', async (t) => { const result = compile(source); t.deepEqual(result, expected); }); + + +test('compile a lazy state ($) expression', (t) => { + const source = 'get($.data.endpoint);'; + const expected = 'export default [get(state => state.data.endpoint)];'; + const result = compile(source); + t.assert(result === expected); +}); \ No newline at end of file diff --git a/packages/compiler/test/transforms/lazy-state.test.ts b/packages/compiler/test/transforms/lazy-state.test.ts new file mode 100644 index 000000000..c5a353ca9 --- /dev/null +++ b/packages/compiler/test/transforms/lazy-state.test.ts @@ -0,0 +1,76 @@ +import test, { ExecutionContext } from 'ava'; +import { print } from 'recast'; +import { namedTypes, NodePath, builders as b } from 'ast-types'; + +import parse from '../../src/parse'; + +import transform from '../../src/transform'; +import visitors from '../../src/transforms/lazy-state'; + +test('convert a simple dollar reference', (t) => { + const ast = parse('get($.data)'); + + const transformed = transform(ast, [visitors]); + const { code } = print(transformed) + t.log(code) + + t.is(code, 'get(state => state.data)') +}) + +test('convert a chained dollar reference', (t) => { + const ast = parse('get($.a.b.c.d)'); + + const transformed = transform(ast, [visitors]); + const { code } = print(transformed) + t.log(code) + + t.is(code, 'get(state => state.a.b.c.d)') +}) + +test('ignore a regular chain reference', (t) => { + const ast = parse('get(a.b.c.d)'); + + const transformed = transform(ast, [visitors]); + const { code } = print(transformed) + t.log(code) + + t.is(code, 'get(a.b.c.d)') +}) + +test('ignore a string', (t) => { + const ast = parse('get("$.a.b")'); + + const transformed = transform(ast, [visitors]); + const { code } = print(transformed) + t.log(code) + + t.is(code, 'get("$.a.b")') +}) + +// TODO do we want to support this? +test('convert a nested dollar reference', (t) => { + const ast = parse(`fn(() => { + get($.data) +})`); + + const transformed = transform(ast, [visitors]); + const { code } = print(transformed) + t.log(code) + + // syntax starts getting a but picky at this level, + // better to do ast tests + t.is(code, `fn(() => { + get(state => state.data) +})`) +}) + +// TODO does our compiler not support optional chaining?? +test.skip('convert an optional chained simple dollar reference', (t) => { + const ast = parse('get($.a?.b.c.d)'); + + // const transformed = transform(ast, [visitors]); + // const { code } = print(transformed) + // t.log(code) + + // t.is(code, 'get(state => state.a?.b.c.d)') +}) \ No newline at end of file