Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: conversion of code step actions #51

Merged
merged 4 commits into from
Apr 25, 2023
Merged
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
29 changes: 29 additions & 0 deletions lib/collections/LegacyAction.js
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,35 @@ const traversalMethods = {
.filter((path) => path.value.id.properties.length === 1);
},

/**
* Finds occurrences of `params` member expressions
*
* `params.__a`
*
* @return {Collection}
*/
getParamsMemberExpressions: function() {
return this.getFunctionExpression()
.getVariableInstances(0)
.map((path) => path.parent)
.filter((path) => types.MemberExpression.check(path.value));
},

/**
* Finds occurrences of `params` variable declarators
*
* `const { __a: __b } = params`
*
* @return {Collection}
*/
getParamsVariableDeclarators: function() {
return this.getFunctionExpression()
.getVariableInstances(0)
.map((path) => path.parent)
.filter((path) => types.VariableDeclarator.check(path.value))
.filter((path) => path.value.id.properties.length === 1);
},

getLegacyCodeCellVars: function(name) {
return this.find(types.Identifier)
.filter((path) => {
Expand Down
66 changes: 50 additions & 16 deletions lib/convert.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ function dbProp() {

function paramTypeToPropType(type) {
const mapping = {
"any": "any",
"array": "any",
"boolean": "boolean",
"integer": "integer",
Expand All @@ -29,7 +30,7 @@ function paramTypeToPropType(type) {
function paramToProp(param, paramKey, required, createLabel) {
const label = param._label ||
(createLabel
? `${key.charAt(0).toUpperCase()}${key.slice(1)}`.replace(/_/g, " ")
? `${paramKey.charAt(0).toUpperCase()}${paramKey.slice(1)}`.replace(/_/g, " ")
: undefined);
// If a prop key contains a colon, its object key must be a string literal
// rather than identifier (e.g. `{ prop:key: {} }` -> `{ "prop:key": {} }`)
Expand Down Expand Up @@ -60,12 +61,15 @@ function authsToAppProps(auths) {
}));
}

function makeComponentSlug(appSlug, namespace) {
function makeComponentSlug(namespace, appSlug="") {
// E.g. `cloudinary_upload_media_asset` -> ``upload-media-asset`
return paramCase(namespace.replace(appSlug, ""));
}

function makeComponentKey(appSlug, componentSlug) {
function makeComponentKey(componentSlug, appSlug) {
if (!appSlug) {
return componentSlug;
}
return `${appSlug}-${componentSlug}`;
}

Expand All @@ -78,51 +82,73 @@ function wrapCodeWithFunctionExpr(source) {

// Wrap code with function Expression
// Get app props from `auths.appName` in code
// If code has this.$checkpoint, add db prop
// If code has this.$checkpoint, add data_store prop called "db"
// Convert params to array of props
// Convert code to component run code
// Generate component file using handlebars template, props, and component run code
// Fix issues/eslint errors in generated component file
/**
* Converts a legacy action to a component.
*
* @param {object} actionConfig
* @param {object} options
* @returns
* @returns the converted component
*/
async function convert({
code: codeRaw,
title,
description,
namespace,
codeConfig: codeConfigString,
versionMajor = 0,
versionMinor = 0,
versionPatch = DEFAULT_VERSION_PATCH,
versionMajor,
versionMinor,
versionPatch,
hashId,
}, {
defineComponent=false,
createLabel=false,
toEsm=true,
usePipedreamLintRules=true,
appPlaceholder=DEFAULT_PLACEHOLDER_APP_SLUG,
addPlaceholderAppProp=false,
}={}) {
const { params_schema: paramsSchema } = JSON.parse(codeConfigString);
// Extract paramsSchema from codeConfig
let paramsSchema;
try {
const codeConfig = JSON.parse(codeConfigString || null);
paramsSchema = codeConfig?.params_schema;
} catch (err) {
throw new Error(`Invalid code config: ${err.message}`);
}
if (typeof paramsSchema !== "object") {
throw new Error("Invalid code config: paramsSchema must be an object");
}

let source = wrapCodeWithFunctionExpr(codeRaw);

const {
auths,
checkpoints
checkpoints,
params,
} = parse(source);

// Get props from code (`auths.app`) & params
if (!paramsSchema) {
// If no params schema config was provided, create one from params used in
// code
paramsSchema = params.reduce((schema, param) => {
schema.properties[param] = { type: "any" };
return schema;
}, { required: [], properties: {} });
}

// Get props from params and code (`auths.app` -> "app",
// `this.$checkpoint`/`$checkpoint` -> "data_store")
const appNames = (auths.length === 0 & addPlaceholderAppProp)
? [appPlaceholder]
: auths;
const appProps = authsToAppProps(appNames);
const appSlug = appProps?.[0]?.key ?? appPlaceholder;
const paramsProps = paramsSchemaToProps(paramsSchema, { createLabel });

const props = [
...appProps,
...(checkpoints.length > 0 ? [dbProp()] : []),
Expand All @@ -131,19 +157,27 @@ async function convert({

const transformedCode = transform(source);

const componentSlug = makeComponentSlug(appSlug, namespace);
const componentKey = makeComponentKey(appSlug, componentSlug);
let componentSlug, componentKey;
if (namespace) {
componentSlug = makeComponentSlug(namespace, appSlug);
componentKey = makeComponentKey(componentSlug, appSlug);
}

const version = (versionMajor != null || versionMinor != null)
? `${versionMajor || 0}.${versionMinor || 0}.${versionPatch || DEFAULT_VERSION_PATCH}`
: undefined;

const componentCode = generate({
code: transformedCode,
props,
name: title,
description: description?.trim(),
key: componentKey,
version: `${versionMajor}.${versionMinor}.${versionPatch}`,
version,
hashId,
}, { defineComponent });

const lintedCode = await fix(componentCode, { toEsm });
const lintedCode = await fix(componentCode, { toEsm, usePipedreamLintRules });

return {
code: lintedCode,
Expand Down
24 changes: 13 additions & 11 deletions lib/fix.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,6 @@ import removeEmptyObject from "./transforms/remove-empty-object.js";
import snakeToCamelCase from "./transforms/snake-to-camel-case.js";
import cjsToEsm from "./transforms/cjs-to-esm.js";

const eslintConfig = JSON.parse(readFile("../resources/.eslint.json", { relative: true }));

const eslint = new ESLint({
useEslintrc: false,
overrideConfig: eslintConfig,
fix: true
});

/**
* Fix some errors that eslint doesn't autofix before linting.
*
Expand Down Expand Up @@ -43,7 +35,13 @@ function preFixErrors(source, { toEsm }={}) {
}


async function lintAndFix(source) {
async function lintAndFix(source, { configFile="../resources/.eslint-pipedream.json" }={}) {
const eslintConfig = JSON.parse(readFile(configFile, { relative: true }));
const eslint = new ESLint({
useEslintrc: false,
overrideConfig: eslintConfig,
fix: true
});
const results = await eslint.lintText(source, { filePath: "components/app_slug/actions/comp_slug/comp_slug.js" });
const [result] = results;
const { output, messages } = result;
Expand All @@ -61,10 +59,14 @@ async function lintAndFix(source) {
* @param {object} [options={}]
* @returns {string}
*/
async function fix(source, { toEsm }={}) {
async function fix(source, { toEsm, usePipedreamLintRules = true }={}) {
let code = fixByteCharacters(source);
code = preFixErrors(code, { toEsm });
code = await lintAndFix(code);
code = await lintAndFix(code, {
configFile: usePipedreamLintRules
? "../resources/.eslint-pipedream.json"
: "../resources/.eslint-default.json"
});
return code;
}

Expand Down
29 changes: 29 additions & 0 deletions lib/getters/params.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import jscodeshift from "jscodeshift/dist/core.js";
import { register } from "../collections/LegacyAction.js";
register();

export default function(fileInfo) {
const j = jscodeshift;

const ast = j(fileInfo.source);

let params = [];

// `params.__a` -> `__a`
params = params.concat(
ast.getParamsMemberExpressions()
.nodes()
.map((n) => n.property.name)
);

// `const { __a: __b } = params` -> `__a`
params = params.concat(
ast.getParamsVariableDeclarators()
.nodes()
.map((n) => n.id.properties[0].key.name)
);

const uniqueParams = [...new Set(params)];

return uniqueParams;
}
3 changes: 3 additions & 0 deletions lib/parse.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import getAuths from "./getters/auths.js";
import getParams from "./getters/params.js";
import getCheckpointAssignments from "./getters/checkpoint-assignments.js";
import getCheckpointExpressions from "./getters/checkpoint-expressions.js";
import getWorkflowCheckpoints from "./getters/workflow-checkpoints.js";
Expand All @@ -8,9 +9,11 @@ function parse(source) {
const checkpointExpressions = getCheckpointExpressions({ source });
const workflowCheckpoints = getWorkflowCheckpoints({ source });
const auths = getAuths({ source });
const params = getParams({ source });
return {
checkpoints: [...checkpointAssignments, ...checkpointExpressions, ...workflowCheckpoints],
auths,
params,
};
}

Expand Down
17 changes: 17 additions & 0 deletions resources/.eslint-default.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"env": {
"commonjs": true,
"es2020": true,
"node": true
},
"parserOptions": {
"ecmaVersion": 12,
"sourceType": "module"
},
"rules": {
"indent": [
"error",
2
]
}
}
File renamed without changes.
10 changes: 10 additions & 0 deletions resources/templates/action-cjs.handlebars
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,28 @@ module.exports = {
{{/if}}

{{~#*inline "componentProperties"}}
{{#if key}}
key: {{tostring key}},
{{/if}}
{{#if name}}
name: {{tostring name}},
{{/if}}
{{#if description}}
description: {{tostring description}},
{{/if}}
{{#if version}}
version: {{tostring version}},
{{/if}}
{{#if key}}
type: "action",
{{/if}}
{{#if props.[0]}}
props: {
{{#each props}}
{{> componentProp }}
{{/each}}
},
{{/if}}
async run({ steps, $ }) {{{code}}}
{{/inline}}

Expand Down