@knocklabs/cli 0.1.0-rc.2 → 0.1.0-rc.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +82 -6
- package/dist/commands/commit/index.js +4 -18
- package/dist/commands/commit/promote.js +4 -17
- package/dist/commands/translation/list.js +82 -0
- package/dist/commands/translation/pull.js +124 -0
- package/dist/commands/translation/push.js +130 -0
- package/dist/commands/translation/validate.js +122 -0
- package/dist/commands/workflow/activate.js +5 -18
- package/dist/commands/workflow/new.js +3 -3
- package/dist/commands/workflow/pull.js +70 -17
- package/dist/commands/workflow/push.js +3 -3
- package/dist/commands/workflow/validate.js +3 -3
- package/dist/lib/api-v1.js +38 -2
- package/dist/lib/base-command.js +2 -2
- package/dist/lib/helpers/error.js +16 -8
- package/dist/lib/helpers/flag.js +63 -3
- package/dist/lib/helpers/fs.js +52 -0
- package/dist/lib/helpers/json.js +6 -2
- package/dist/lib/helpers/object.js +43 -0
- package/dist/lib/helpers/page.js +3 -1
- package/dist/lib/helpers/request.js +17 -49
- package/dist/lib/helpers/ux.js +42 -0
- package/dist/lib/marshal/translation/helpers.js +185 -0
- package/dist/lib/marshal/translation/index.js +19 -0
- package/dist/lib/marshal/translation/reader.js +118 -0
- package/dist/lib/marshal/translation/types.js +4 -0
- package/dist/lib/marshal/translation/writer.js +86 -0
- package/dist/lib/marshal/workflow/generator.js +46 -5
- package/dist/lib/marshal/workflow/helpers.js +2 -0
- package/dist/lib/marshal/workflow/reader.js +136 -117
- package/dist/lib/marshal/workflow/writer.js +235 -98
- package/dist/lib/{helpers/dir-context.js → run-context/helpers.js} +1 -1
- package/dist/lib/run-context/index.js +22 -0
- package/dist/lib/{run-context.js → run-context/loader.js} +22 -7
- package/dist/lib/run-context/types.js +4 -0
- package/oclif.manifest.json +253 -1
- package/package.json +11 -10
- package/dist/lib/helpers/spinner.js +0 -20
|
@@ -9,9 +9,9 @@ function _export(target, all) {
|
|
|
9
9
|
});
|
|
10
10
|
}
|
|
11
11
|
_export(exports, {
|
|
12
|
-
validateTemplateFilePathFormat: ()=>validateTemplateFilePathFormat,
|
|
13
12
|
readWorkflowDir: ()=>readWorkflowDir,
|
|
14
|
-
|
|
13
|
+
checkIfValidExtractedFilePathFormat: ()=>checkIfValidExtractedFilePathFormat,
|
|
14
|
+
readExtractedFileSync: ()=>readExtractedFileSync
|
|
15
15
|
});
|
|
16
16
|
const _nodePath = /*#__PURE__*/ _interopRequireWildcard(require("node:path"));
|
|
17
17
|
const _fsExtra = /*#__PURE__*/ _interopRequireWildcard(require("fs-extra"));
|
|
@@ -21,7 +21,6 @@ const _json = require("../../helpers/json");
|
|
|
21
21
|
const _liquid = require("../../helpers/liquid");
|
|
22
22
|
const _object = require("../../helpers/object");
|
|
23
23
|
const _helpers = require("./helpers");
|
|
24
|
-
const _types = require("./types");
|
|
25
24
|
function _getRequireWildcardCache(nodeInterop) {
|
|
26
25
|
if (typeof WeakMap !== "function") return null;
|
|
27
26
|
var cacheBabelInterop = new WeakMap();
|
|
@@ -61,66 +60,95 @@ function _interopRequireWildcard(obj, nodeInterop) {
|
|
|
61
60
|
}
|
|
62
61
|
return newObj;
|
|
63
62
|
}
|
|
64
|
-
|
|
63
|
+
// For now we support up to two levels of content extraction in workflow.json.
|
|
64
|
+
// (e.g. workflow.json, then visual_blocks.json)
|
|
65
|
+
const MAX_EXTRACTION_LEVEL = 2;
|
|
66
|
+
// The following files are exepected to have valid json content, and should be
|
|
67
|
+
// decoded and joined into the main workflow.json.
|
|
68
|
+
const DECODABLE_JSON_FILES = new Set([
|
|
69
|
+
_helpers.VISUAL_BLOCKS_JSON
|
|
70
|
+
]);
|
|
71
|
+
/*
|
|
72
|
+
* Validate the file path format of an extracted field. The file path must be:
|
|
73
|
+
*
|
|
74
|
+
* 1) Expressed as a relative path.
|
|
75
|
+
*
|
|
76
|
+
* For exmaple:
|
|
77
|
+
* subject@: "email_1/subject.html" // GOOD
|
|
78
|
+
* subject@: "./email_1/subject.html" // GOOD
|
|
79
|
+
* subject@: "/workflow-x/email_1/subject.html" // BAD
|
|
80
|
+
*
|
|
81
|
+
* 2) The resolved path must be contained inside the workflow directory
|
|
82
|
+
*
|
|
83
|
+
* For exmaple (workflow-y is a different workflow dir in this example):
|
|
84
|
+
* subject@: "./email_1/subject.html" // GOOD
|
|
85
|
+
* subject@: "../workflow-y/email_1/subject.html" // BAD
|
|
86
|
+
*
|
|
87
|
+
* Note: does not validate the presence of the file nor the uniqueness of the
|
|
88
|
+
* file path.
|
|
89
|
+
*/ const checkIfValidExtractedFilePathFormat = (relpath, sourceFileAbspath)=>{
|
|
65
90
|
if (typeof relpath !== "string") return false;
|
|
66
91
|
if (_nodePath.isAbsolute(relpath)) return false;
|
|
67
|
-
const
|
|
68
|
-
const pathDiff = _nodePath.relative(
|
|
92
|
+
const extractedFileAbspath = _nodePath.resolve(sourceFileAbspath, relpath);
|
|
93
|
+
const pathDiff = _nodePath.relative(sourceFileAbspath, extractedFileAbspath);
|
|
69
94
|
return !pathDiff.startsWith("..");
|
|
70
95
|
};
|
|
71
96
|
/*
|
|
72
|
-
* Validate
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
97
|
+
* Validate the extracted file path based on its format and uniqueness (but not
|
|
98
|
+
* the presence).
|
|
99
|
+
*
|
|
100
|
+
* Note, the uniqueness check is based on reading from and writing to
|
|
101
|
+
* uniqueFilePaths, which is MUTATED in place.
|
|
102
|
+
*/ const validateExtractedFilePath = (val, workflowDirCtx, uniqueFilePaths, objPathToFieldStr)=>{
|
|
103
|
+
const workflowJsonPath = _nodePath.resolve(workflowDirCtx.abspath, _helpers.WORKFLOW_JSON);
|
|
104
|
+
// Validate the file path format, and that it is unique per workflow.
|
|
105
|
+
if (!checkIfValidExtractedFilePathFormat(val, workflowJsonPath) || typeof val !== "string" || val in uniqueFilePaths) {
|
|
106
|
+
const error = new _error.JsonDataError("must be a relative path string to a unique file within the directory", objPathToFieldStr);
|
|
107
|
+
return error;
|
|
108
|
+
}
|
|
109
|
+
// Keep track of all the valid extracted file paths that have been seen, so
|
|
110
|
+
// we can validate each file path's uniqueness as we traverse.
|
|
111
|
+
uniqueFilePaths[val] = true;
|
|
112
|
+
return undefined;
|
|
76
113
|
};
|
|
77
|
-
const
|
|
114
|
+
const readExtractedFileSync = (relpath, workflowDirCtx, objPathToFieldStr = "")=>{
|
|
115
|
+
// Check if the file actually exists at the given file path.
|
|
78
116
|
const abspath = _nodePath.resolve(workflowDirCtx.abspath, relpath);
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
const liquidParseError = (0, _liquid.validateLiquidSyntax)(content);
|
|
83
|
-
return liquidParseError ? [
|
|
84
|
-
undefined,
|
|
85
|
-
[
|
|
86
|
-
liquidParseError
|
|
87
|
-
]
|
|
88
|
-
] : [
|
|
89
|
-
content,
|
|
90
|
-
[]
|
|
91
|
-
];
|
|
92
|
-
};
|
|
93
|
-
/*
|
|
94
|
-
* Validates that a given value is a valid template file path and the file
|
|
95
|
-
* actually exists, before reading the file content.
|
|
96
|
-
*/ const maybeReadTemplateFile = async (val, workflowDirCtx, extractedFilePaths, pathToFieldStr)=>{
|
|
97
|
-
// Validate the file path format, and that it is unique per workflow.
|
|
98
|
-
if (!validateTemplateFilePathFormat(val, workflowDirCtx) || typeof val !== "string" || val in extractedFilePaths) {
|
|
99
|
-
const error = new _error.JsonDataError("must be a relative path string to a unique file within the directory", pathToFieldStr);
|
|
117
|
+
const exists = _fsExtra.pathExistsSync(abspath);
|
|
118
|
+
if (!exists) {
|
|
119
|
+
const error = new _error.JsonDataError("must be a relative path string to a file that exists", objPathToFieldStr);
|
|
100
120
|
return [
|
|
101
121
|
undefined,
|
|
102
122
|
error
|
|
103
123
|
];
|
|
104
124
|
}
|
|
105
|
-
//
|
|
106
|
-
//
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
125
|
+
// Read the file and check for valid liquid syntax given it is supported
|
|
126
|
+
// across all message templates and file extensions.
|
|
127
|
+
const contentStr = _fsExtra.readFileSync(abspath, "utf8");
|
|
128
|
+
const liquidParseError = (0, _liquid.validateLiquidSyntax)(contentStr);
|
|
129
|
+
if (liquidParseError) {
|
|
130
|
+
const error = new _error.JsonDataError(`points to a file that contains invalid liquid syntax (${relpath})\n\n` + (0, _error.formatErrors)([
|
|
131
|
+
liquidParseError
|
|
132
|
+
], {
|
|
133
|
+
indentBy: 2
|
|
134
|
+
}), objPathToFieldStr);
|
|
112
135
|
return [
|
|
113
136
|
undefined,
|
|
114
137
|
error
|
|
115
138
|
];
|
|
116
139
|
}
|
|
117
|
-
//
|
|
118
|
-
//
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
140
|
+
// If the file is expected to contain decodable json, then parse the contentStr
|
|
141
|
+
// as such.
|
|
142
|
+
const fileName = _nodePath.basename(abspath.toLowerCase());
|
|
143
|
+
const decodable = DECODABLE_JSON_FILES.has(fileName);
|
|
144
|
+
const [content, jsonParseErrors] = decodable ? (0, _json.parseJson)(contentStr) : [
|
|
145
|
+
contentStr,
|
|
146
|
+
[]
|
|
147
|
+
];
|
|
148
|
+
if (jsonParseErrors.length > 0) {
|
|
149
|
+
const error = new _error.JsonDataError(`points to a file with invalid content (${relpath})\n\n` + (0, _error.formatErrors)(jsonParseErrors, {
|
|
122
150
|
indentBy: 2
|
|
123
|
-
}),
|
|
151
|
+
}), objPathToFieldStr);
|
|
124
152
|
return [
|
|
125
153
|
undefined,
|
|
126
154
|
error
|
|
@@ -131,79 +159,70 @@ const readTemplateFile = async (relpath, workflowDirCtx)=>{
|
|
|
131
159
|
undefined
|
|
132
160
|
];
|
|
133
161
|
};
|
|
134
|
-
const
|
|
162
|
+
const joinExtractedFiles = async (workflowDirCtx, workflowJson)=>{
|
|
163
|
+
// Tracks any errors encountered during traversal. Mutated in place.
|
|
135
164
|
const errors = [];
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
//
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
continue;
|
|
176
|
-
}
|
|
177
|
-
// 3. For a given template, look for any extracted template content, read
|
|
178
|
-
// the extracted template files, then inline the content.
|
|
179
|
-
objPath.push("template");
|
|
180
|
-
for (const [field, val] of Object.entries(step.template)){
|
|
181
|
-
if (field.startsWith("settings")) continue;
|
|
182
|
-
if (!_helpers.FILEPATH_MARKED_RE.test(field)) continue;
|
|
183
|
-
const pathToFieldStr = objPath.to(field).str;
|
|
184
|
-
// eslint-disable-next-line no-await-in-loop
|
|
185
|
-
const [content, error] = await maybeReadTemplateFile(val, workflowDirCtx, extractedFilePaths, pathToFieldStr);
|
|
186
|
-
if (error) {
|
|
187
|
-
errors.push(error);
|
|
188
|
-
continue;
|
|
165
|
+
// Tracks each new valid extracted file path seen (rebased to be relative to
|
|
166
|
+
// workflow.json) in the workflow json node. Mutated in place, and used
|
|
167
|
+
// to validate the uniqueness of an extracted path encountered.
|
|
168
|
+
const uniqueFilePaths = {};
|
|
169
|
+
// Tracks each extracted file path (rebased) that gets inlined with its object
|
|
170
|
+
// path location, per each traversal iteration. Mutated in place, and used for
|
|
171
|
+
// rebasing an extracted path to be relative to the location of the workflow
|
|
172
|
+
// json file.
|
|
173
|
+
const joinedFilePathsPerLevel = [];
|
|
174
|
+
for (const [idx] of Array.from({
|
|
175
|
+
length: MAX_EXTRACTION_LEVEL
|
|
176
|
+
}).entries()){
|
|
177
|
+
const currJoinedFilePaths = {};
|
|
178
|
+
const prevJoinedFilePaths = joinedFilePathsPerLevel[idx - 1] || {};
|
|
179
|
+
(0, _object.mapValuesDeep)(workflowJson, (value, key, parts)=>{
|
|
180
|
+
// If not marked with the @ suffix, there's nothing to do.
|
|
181
|
+
if (!_helpers.FILEPATH_MARKED_RE.test(key)) return;
|
|
182
|
+
const objPathToFieldStr = _object.ObjPath.stringify(parts);
|
|
183
|
+
const inlinObjPathStr = objPathToFieldStr.replace(_helpers.FILEPATH_MARKED_RE, "");
|
|
184
|
+
// If there is inlined content present already, then nothing more to do.
|
|
185
|
+
if ((0, _lodash.hasIn)(workflowJson, inlinObjPathStr)) return;
|
|
186
|
+
// Check if the extracted path found at the current field path belongs to
|
|
187
|
+
// a node whose parent or grandparent has been previously joined earlier
|
|
188
|
+
// in the tree. If so, rebase the extracted path to be a relative path to
|
|
189
|
+
// the workflow json.
|
|
190
|
+
const lastFound = (0, _object.getLastFound)(prevJoinedFilePaths, parts);
|
|
191
|
+
const prevJoinedFilePath = typeof lastFound === "string" ? lastFound : undefined;
|
|
192
|
+
const rebasedFilePath = prevJoinedFilePath ? _nodePath.join(_nodePath.dirname(prevJoinedFilePath), value) : value;
|
|
193
|
+
const invalidFilePathError = validateExtractedFilePath(rebasedFilePath, workflowDirCtx, uniqueFilePaths, objPathToFieldStr);
|
|
194
|
+
if (invalidFilePathError) {
|
|
195
|
+
errors.push(invalidFilePathError);
|
|
196
|
+
// Wipe the invalid file path in the node so the final workflow json
|
|
197
|
+
// object ends up with only valid file paths, this way workflow writer
|
|
198
|
+
// can see only valid file paths and use those when pulling. Also set
|
|
199
|
+
// the inlined field path in workflow object with empty content so we
|
|
200
|
+
// know we've looked at this extracted file path.
|
|
201
|
+
(0, _lodash.set)(workflowJson, objPathToFieldStr, undefined);
|
|
202
|
+
(0, _lodash.set)(workflowJson, inlinObjPathStr, undefined);
|
|
203
|
+
return;
|
|
189
204
|
}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
errors.push(error);
|
|
202
|
-
continue;
|
|
205
|
+
// By this point we have a valid extracted file path, so attempt to read
|
|
206
|
+
// the file at the file path.
|
|
207
|
+
const [content, readExtractedFileError] = readExtractedFileSync(rebasedFilePath, workflowDirCtx, objPathToFieldStr);
|
|
208
|
+
if (readExtractedFileError) {
|
|
209
|
+
errors.push(readExtractedFileError);
|
|
210
|
+
// Replace the extracted file path with the rebased one, and set the
|
|
211
|
+
// inlined field path in workflow object with empty content, so we know
|
|
212
|
+
// we do not need to try inlining again.
|
|
213
|
+
(0, _lodash.set)(workflowJson, objPathToFieldStr, rebasedFilePath);
|
|
214
|
+
(0, _lodash.set)(workflowJson, inlinObjPathStr, undefined);
|
|
215
|
+
return;
|
|
203
216
|
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
217
|
+
// Inline the file content and replace the extracted file path with a
|
|
218
|
+
// rebased one.
|
|
219
|
+
(0, _lodash.set)(workflowJson, objPathToFieldStr, rebasedFilePath);
|
|
220
|
+
(0, _lodash.set)(workflowJson, inlinObjPathStr, content);
|
|
221
|
+
// Track joined file paths from the current join level.
|
|
222
|
+
(0, _lodash.set)(currJoinedFilePaths, inlinObjPathStr, rebasedFilePath);
|
|
223
|
+
});
|
|
224
|
+
// Finally save all the joined file paths from this traversal iteration.
|
|
225
|
+
joinedFilePathsPerLevel[idx] = currJoinedFilePaths;
|
|
207
226
|
}
|
|
208
227
|
return [
|
|
209
228
|
workflowJson,
|
|
@@ -212,7 +231,7 @@ const compileTemplateFiles = async (workflowDirCtx, workflowJson)=>{
|
|
|
212
231
|
};
|
|
213
232
|
const readWorkflowDir = async (workflowDirCtx, opts = {})=>{
|
|
214
233
|
const { abspath } = workflowDirCtx;
|
|
215
|
-
const {
|
|
234
|
+
const { withExtractedFiles =false , withReadonlyField =false } = opts;
|
|
216
235
|
const dirExists = await _fsExtra.pathExists(abspath);
|
|
217
236
|
if (!dirExists) throw new Error(`${abspath} does not exist`);
|
|
218
237
|
const workflowJsonPath = await (0, _helpers.lsWorkflowJson)(abspath);
|
|
@@ -223,7 +242,7 @@ const readWorkflowDir = async (workflowDirCtx, opts = {})=>{
|
|
|
223
242
|
workflowJson = withReadonlyField ? workflowJson : (0, _object.omitDeep)(workflowJson, [
|
|
224
243
|
"__readonly"
|
|
225
244
|
]);
|
|
226
|
-
return
|
|
245
|
+
return withExtractedFiles ? joinExtractedFiles(workflowDirCtx, workflowJson) : [
|
|
227
246
|
workflowJson,
|
|
228
247
|
[]
|
|
229
248
|
];
|