zapier-platform-cli 15.18.0 → 15.19.0
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/oclif.manifest.json +1 -1
- package/package.json +5 -2
- package/scaffold/create.template.ts +64 -0
- package/scaffold/resource.template.ts +119 -0
- package/scaffold/search.template.ts +63 -0
- package/scaffold/test.template.ts +18 -0
- package/scaffold/trigger.template.ts +58 -0
- package/src/oclif/commands/invoke.js +39 -14
- package/src/oclif/commands/scaffold.js +98 -90
- package/src/utils/ast.js +105 -4
- package/src/utils/scaffold.js +290 -37
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import zapier from 'zapier-platform-core';
|
|
3
|
+
|
|
4
|
+
import App from '../../index';
|
|
5
|
+
|
|
6
|
+
const appTester = zapier.createAppTester(App);
|
|
7
|
+
// read the `.env` file into the environment, if available
|
|
8
|
+
zapier.tools.env.inject();
|
|
9
|
+
|
|
10
|
+
describe('<%= ACTION_PLURAL %>.<%= KEY %>', () => {
|
|
11
|
+
it('should run', async () => {
|
|
12
|
+
const bundle = { inputData: {} };
|
|
13
|
+
|
|
14
|
+
const results = await appTester(App.<%= ACTION_PLURAL %>['<%= KEY %>'].<%= MAYBE_RESOURCE %>operation.perform, bundle);
|
|
15
|
+
expect(results).toBeDefined();
|
|
16
|
+
// TODO: add more assertions
|
|
17
|
+
});
|
|
18
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { PerformFunction, Trigger } from 'zapier-platform-core';
|
|
2
|
+
|
|
3
|
+
// triggers on a new <%= LOWER_NOUN %> with a certain tag
|
|
4
|
+
const perform: PerformFunction = async (z, bundle) => {
|
|
5
|
+
const response = await z.request({
|
|
6
|
+
url: 'https://jsonplaceholder.typicode.com/posts',
|
|
7
|
+
params: {
|
|
8
|
+
tag: bundle.inputData.tagName,
|
|
9
|
+
},
|
|
10
|
+
});
|
|
11
|
+
// this should return an array of objects
|
|
12
|
+
return response.data;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export default {
|
|
16
|
+
// see here for a full list of available properties:
|
|
17
|
+
// https://github.com/zapier/zapier-platform/blob/main/packages/schema/docs/build/schema.md#triggerschema
|
|
18
|
+
key: '<%= KEY %>' as const,
|
|
19
|
+
noun: '<%= NOUN %>',
|
|
20
|
+
|
|
21
|
+
display: {
|
|
22
|
+
label: 'New <%= NOUN %>',
|
|
23
|
+
description: 'Triggers when a new <%= LOWER_NOUN %> is created.',
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
operation: {
|
|
27
|
+
type: 'polling',
|
|
28
|
+
perform,
|
|
29
|
+
|
|
30
|
+
<%= INCLUDE_INTRO_COMMENTS ? [
|
|
31
|
+
'// `inputFields` defines the fields a user could provide',
|
|
32
|
+
'// Zapier will pass them in as `bundle.inputData` later. They\'re optional.'
|
|
33
|
+
].join('\n ') : '' %>
|
|
34
|
+
inputFields: [],
|
|
35
|
+
|
|
36
|
+
<%= INCLUDE_INTRO_COMMENTS ? [
|
|
37
|
+
'// In cases where Zapier needs to show an example record to the user, but we are unable to get a live example',
|
|
38
|
+
'// from the API, Zapier will fallback to this hard-coded sample. It should reflect the data structure of',
|
|
39
|
+
'// returned records, and have obvious placeholder values that we can show to any user.'
|
|
40
|
+
].join('\n ') : '' %>
|
|
41
|
+
sample: {
|
|
42
|
+
id: 1,
|
|
43
|
+
name: 'Test',
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
<%= INCLUDE_INTRO_COMMENTS ? [
|
|
47
|
+
'// If fields are custom to each user (like spreadsheet columns), `outputFields` can create human labels',
|
|
48
|
+
'// For a more complete example of using dynamic fields see',
|
|
49
|
+
'// https://github.com/zapier/zapier-platform/tree/main/packages/cli#customdynamic-fields',
|
|
50
|
+
'// Alternatively, a static field definition can be provided, to specify labels for the fields'
|
|
51
|
+
].join('\n ') : '' %>
|
|
52
|
+
outputFields: [
|
|
53
|
+
// these are placeholders to match the example `perform` above
|
|
54
|
+
// {key: 'id', label: 'Person ID'},
|
|
55
|
+
// {key: 'name', label: 'Person Name'}
|
|
56
|
+
],
|
|
57
|
+
},
|
|
58
|
+
} satisfies Trigger;
|
|
@@ -249,6 +249,7 @@ const testAuth = async (authData, meta, zcacheTestObj) => {
|
|
|
249
249
|
},
|
|
250
250
|
zcacheTestObj,
|
|
251
251
|
customLogger,
|
|
252
|
+
calledFromCliInvoke: true,
|
|
252
253
|
});
|
|
253
254
|
endSpinner();
|
|
254
255
|
return result;
|
|
@@ -367,7 +368,7 @@ class InvokeCommand extends BaseCommand {
|
|
|
367
368
|
]);
|
|
368
369
|
}
|
|
369
370
|
|
|
370
|
-
async startCustomAuth(authFields) {
|
|
371
|
+
async startCustomAuth(authFields, zcacheTestObj) {
|
|
371
372
|
if (this.nonInteractive) {
|
|
372
373
|
throw new Error(
|
|
373
374
|
'The `auth start` subcommand for "custom" authentication type only works in interactive mode.'
|
|
@@ -376,7 +377,7 @@ class InvokeCommand extends BaseCommand {
|
|
|
376
377
|
return this.promptForAuthFields(authFields);
|
|
377
378
|
}
|
|
378
379
|
|
|
379
|
-
async startOAuth2(appDefinition) {
|
|
380
|
+
async startOAuth2(appDefinition, zcacheTestObj) {
|
|
380
381
|
const redirectUri = this.flags['redirect-uri'];
|
|
381
382
|
const port = this.flags['local-port'];
|
|
382
383
|
const env = {};
|
|
@@ -428,6 +429,9 @@ class InvokeCommand extends BaseCommand {
|
|
|
428
429
|
state: stateParam,
|
|
429
430
|
},
|
|
430
431
|
},
|
|
432
|
+
zcacheTestObj,
|
|
433
|
+
customLogger,
|
|
434
|
+
calledFromCliInvoke: true,
|
|
431
435
|
});
|
|
432
436
|
if (!authorizeUrl.includes('&scope=')) {
|
|
433
437
|
const scope = appDefinition.authentication.oauth2Config.scope;
|
|
@@ -499,13 +503,16 @@ class InvokeCommand extends BaseCommand {
|
|
|
499
503
|
redirect_uri: redirectUri,
|
|
500
504
|
},
|
|
501
505
|
},
|
|
506
|
+
zcacheTestObj,
|
|
507
|
+
customLogger,
|
|
508
|
+
calledFromCliInvoke: true,
|
|
502
509
|
});
|
|
503
510
|
|
|
504
511
|
endSpinner();
|
|
505
512
|
return authData;
|
|
506
513
|
}
|
|
507
514
|
|
|
508
|
-
async startSessionAuth(appDefinition) {
|
|
515
|
+
async startSessionAuth(appDefinition, zcacheTestObj) {
|
|
509
516
|
if (this.nonInteractive) {
|
|
510
517
|
throw new Error(
|
|
511
518
|
'The `auth start` subcommand for "session" authentication type only works in interactive mode.'
|
|
@@ -522,13 +529,16 @@ class InvokeCommand extends BaseCommand {
|
|
|
522
529
|
bundle: {
|
|
523
530
|
authData,
|
|
524
531
|
},
|
|
532
|
+
zcacheTestObj,
|
|
533
|
+
customLogger,
|
|
534
|
+
calledFromCliInvoke: true,
|
|
525
535
|
});
|
|
526
536
|
endSpinner();
|
|
527
537
|
|
|
528
538
|
return { ...authData, ...sessionData };
|
|
529
539
|
}
|
|
530
540
|
|
|
531
|
-
async startAuth(appDefinition) {
|
|
541
|
+
async startAuth(appDefinition, zcacheTestObj) {
|
|
532
542
|
const authentication = appDefinition.authentication;
|
|
533
543
|
if (!authentication) {
|
|
534
544
|
console.warn(
|
|
@@ -542,11 +552,11 @@ class InvokeCommand extends BaseCommand {
|
|
|
542
552
|
case 'basic':
|
|
543
553
|
return this.startBasicAuth(authentication.fields);
|
|
544
554
|
case 'custom':
|
|
545
|
-
return this.startCustomAuth(authentication.fields);
|
|
555
|
+
return this.startCustomAuth(authentication.fields, zcacheTestObj);
|
|
546
556
|
case 'oauth2':
|
|
547
|
-
return this.startOAuth2(appDefinition);
|
|
557
|
+
return this.startOAuth2(appDefinition, zcacheTestObj);
|
|
548
558
|
case 'session':
|
|
549
|
-
return this.startSessionAuth(appDefinition);
|
|
559
|
+
return this.startSessionAuth(appDefinition, zcacheTestObj);
|
|
550
560
|
default:
|
|
551
561
|
// TODO: Add support for 'digest' and 'oauth1'
|
|
552
562
|
throw new Error(
|
|
@@ -555,7 +565,7 @@ class InvokeCommand extends BaseCommand {
|
|
|
555
565
|
}
|
|
556
566
|
}
|
|
557
567
|
|
|
558
|
-
async refreshOAuth2(appDefinition, authData) {
|
|
568
|
+
async refreshOAuth2(appDefinition, authData, zcacheTestObj) {
|
|
559
569
|
startSpinner('Invoking authentication.oauth2Config.refreshAccessToken');
|
|
560
570
|
|
|
561
571
|
const newAuthData = await localAppCommand({
|
|
@@ -564,13 +574,16 @@ class InvokeCommand extends BaseCommand {
|
|
|
564
574
|
bundle: {
|
|
565
575
|
authData,
|
|
566
576
|
},
|
|
577
|
+
zcacheTestObj,
|
|
578
|
+
customLogger,
|
|
579
|
+
calledFromCliInvoke: true,
|
|
567
580
|
});
|
|
568
581
|
|
|
569
582
|
endSpinner();
|
|
570
583
|
return newAuthData;
|
|
571
584
|
}
|
|
572
585
|
|
|
573
|
-
async refreshSessionAuth(appDefinition, authData) {
|
|
586
|
+
async refreshSessionAuth(appDefinition, authData, zcacheTestObj) {
|
|
574
587
|
startSpinner('Invoking authentication.sessionConfig.perform');
|
|
575
588
|
|
|
576
589
|
const sessionData = await localAppCommand({
|
|
@@ -579,13 +592,16 @@ class InvokeCommand extends BaseCommand {
|
|
|
579
592
|
bundle: {
|
|
580
593
|
authData,
|
|
581
594
|
},
|
|
595
|
+
zcacheTestObj,
|
|
596
|
+
customLogger,
|
|
597
|
+
calledFromCliInvoke: true,
|
|
582
598
|
});
|
|
583
599
|
|
|
584
600
|
endSpinner();
|
|
585
601
|
return sessionData;
|
|
586
602
|
}
|
|
587
603
|
|
|
588
|
-
async refreshAuth(appDefinition, authData) {
|
|
604
|
+
async refreshAuth(appDefinition, authData, zcacheTestObj) {
|
|
589
605
|
const authentication = appDefinition.authentication;
|
|
590
606
|
if (!authentication) {
|
|
591
607
|
console.warn(
|
|
@@ -602,9 +618,9 @@ class InvokeCommand extends BaseCommand {
|
|
|
602
618
|
}
|
|
603
619
|
switch (authentication.type) {
|
|
604
620
|
case 'oauth2':
|
|
605
|
-
return this.refreshOAuth2(appDefinition, authData);
|
|
621
|
+
return this.refreshOAuth2(appDefinition, authData, zcacheTestObj);
|
|
606
622
|
case 'session':
|
|
607
|
-
return this.refreshSessionAuth(appDefinition, authData);
|
|
623
|
+
return this.refreshSessionAuth(appDefinition, authData, zcacheTestObj);
|
|
608
624
|
default:
|
|
609
625
|
throw new Error(
|
|
610
626
|
`This command doesn't support refreshing authentication type "${authentication.type}".`
|
|
@@ -840,6 +856,7 @@ class InvokeCommand extends BaseCommand {
|
|
|
840
856
|
zcacheTestObj,
|
|
841
857
|
cursorTestObj,
|
|
842
858
|
customLogger,
|
|
859
|
+
calledFromCliInvoke: true,
|
|
843
860
|
});
|
|
844
861
|
endSpinner();
|
|
845
862
|
|
|
@@ -873,6 +890,7 @@ class InvokeCommand extends BaseCommand {
|
|
|
873
890
|
zcacheTestObj,
|
|
874
891
|
cursorTestObj,
|
|
875
892
|
customLogger,
|
|
893
|
+
calledFromCliInvoke: true,
|
|
876
894
|
});
|
|
877
895
|
endSpinner();
|
|
878
896
|
|
|
@@ -952,7 +970,10 @@ class InvokeCommand extends BaseCommand {
|
|
|
952
970
|
};
|
|
953
971
|
switch (actionKey) {
|
|
954
972
|
case 'start': {
|
|
955
|
-
const newAuthData = await this.startAuth(
|
|
973
|
+
const newAuthData = await this.startAuth(
|
|
974
|
+
appDefinition,
|
|
975
|
+
zcacheTestObj
|
|
976
|
+
);
|
|
956
977
|
if (_.isEmpty(newAuthData)) {
|
|
957
978
|
return;
|
|
958
979
|
}
|
|
@@ -963,7 +984,11 @@ class InvokeCommand extends BaseCommand {
|
|
|
963
984
|
return;
|
|
964
985
|
}
|
|
965
986
|
case 'refresh': {
|
|
966
|
-
const newAuthData = await this.refreshAuth(
|
|
987
|
+
const newAuthData = await this.refreshAuth(
|
|
988
|
+
appDefinition,
|
|
989
|
+
authData,
|
|
990
|
+
zcacheTestObj
|
|
991
|
+
);
|
|
967
992
|
if (_.isEmpty(newAuthData)) {
|
|
968
993
|
return;
|
|
969
994
|
}
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
+
/* eslint-disable camelcase */
|
|
2
|
+
// @ts-check
|
|
3
|
+
|
|
1
4
|
const path = require('path');
|
|
5
|
+
const fs = require('fs');
|
|
2
6
|
|
|
3
7
|
const { flags } = require('@oclif/command');
|
|
4
8
|
|
|
@@ -6,8 +10,7 @@ const BaseCommand = require('../ZapierBaseCommand');
|
|
|
6
10
|
const { buildFlags } = require('../buildFlags');
|
|
7
11
|
|
|
8
12
|
const {
|
|
9
|
-
|
|
10
|
-
getRelativeRequirePath,
|
|
13
|
+
createScaffoldingContext,
|
|
11
14
|
plural,
|
|
12
15
|
updateEntryFile,
|
|
13
16
|
isValidEntryFileUpdate,
|
|
@@ -18,126 +21,89 @@ const { isValidAppInstall } = require('../../utils/misc');
|
|
|
18
21
|
const { writeFile } = require('../../utils/files');
|
|
19
22
|
const { ISSUES_URL } = require('../../constants');
|
|
20
23
|
|
|
21
|
-
const getNewFileDirectory = (action, test = false) =>
|
|
22
|
-
path.join(test ? 'test/' : '', plural(action));
|
|
23
|
-
|
|
24
|
-
const getLocalFilePath = (directory, actionKey) =>
|
|
25
|
-
path.join(directory, actionKey);
|
|
26
|
-
/**
|
|
27
|
-
* both the string to `require` and later, the filepath to write to
|
|
28
|
-
*/
|
|
29
|
-
const getFullActionFilePath = (directory, actionKey) =>
|
|
30
|
-
path.join(process.cwd(), getLocalFilePath(directory, actionKey));
|
|
31
|
-
|
|
32
|
-
const getFullActionFilePathWithExtension = (directory, actionKey, isTest) =>
|
|
33
|
-
`${getFullActionFilePath(directory, actionKey)}${isTest ? '.test' : ''}.js`;
|
|
34
|
-
|
|
35
24
|
class ScaffoldCommand extends BaseCommand {
|
|
36
25
|
async perform() {
|
|
37
26
|
const { actionType, noun } = this.args;
|
|
38
|
-
|
|
39
|
-
// TODO: interactive portion here?
|
|
27
|
+
const indexFileLocal = this.flags.entry ?? this.defaultIndexFileLocal();
|
|
40
28
|
const {
|
|
41
|
-
dest:
|
|
42
|
-
|
|
43
|
-
entry = 'index.js',
|
|
29
|
+
dest: actionDirLocal = this.defaultActionDirLocal(indexFileLocal),
|
|
30
|
+
'test-dest': testDirLocal = this.defaultTestDirLocal(indexFileLocal),
|
|
44
31
|
force,
|
|
45
32
|
} = this.flags;
|
|
46
33
|
|
|
47
|
-
|
|
48
|
-
// const tsParser = j.withParser('ts')
|
|
49
|
-
// tsParser(codeStr)
|
|
50
|
-
// will have to change logic probably though
|
|
51
|
-
if (entry.endsWith('ts')) {
|
|
52
|
-
this.error(
|
|
53
|
-
`Typescript isn't supported for scaffolding yet. Instead, try copying the example code at https://github.com/zapier/zapier-platform/blob/b8224ec9855be91c66c924b731199a068b1e913a/example-apps/typescript/src/resources/recipe.ts`
|
|
54
|
-
);
|
|
55
|
-
}
|
|
34
|
+
const language = indexFileLocal.endsWith('.ts') ? 'ts' : 'js';
|
|
56
35
|
|
|
57
|
-
const
|
|
58
|
-
const templateContext = createTemplateContext(
|
|
36
|
+
const context = createScaffoldingContext({
|
|
59
37
|
actionType,
|
|
60
38
|
noun,
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
39
|
+
language,
|
|
40
|
+
indexFileLocal,
|
|
41
|
+
actionDirLocal,
|
|
42
|
+
testDirLocal,
|
|
43
|
+
includeIntroComments: !this.flags['no-help'],
|
|
44
|
+
preventOverwrite: !force,
|
|
45
|
+
});
|
|
65
46
|
|
|
66
|
-
const preventOverwrite = !force;
|
|
67
47
|
// TODO: read from config file?
|
|
68
48
|
|
|
69
|
-
this.startSpinner(
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
actionType,
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
);
|
|
49
|
+
this.startSpinner(`Creating new file: ${context.actionFileLocal}`);
|
|
50
|
+
|
|
51
|
+
await writeTemplateFile({
|
|
52
|
+
destinationPath: context.actionFileResolved,
|
|
53
|
+
templateType: context.actionType,
|
|
54
|
+
language: context.language,
|
|
55
|
+
preventOverwrite: context.preventOverwrite,
|
|
56
|
+
templateContext: context.templateContext,
|
|
57
|
+
});
|
|
78
58
|
this.stopSpinner();
|
|
79
59
|
|
|
80
|
-
this.startSpinner(
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
templateContext,
|
|
89
|
-
getFullActionFilePathWithExtension(newTestActionDir, actionKey, true),
|
|
90
|
-
preventOverwrite
|
|
91
|
-
);
|
|
60
|
+
this.startSpinner(`Creating new test file: ${context.testFileLocal}`);
|
|
61
|
+
await writeTemplateFile({
|
|
62
|
+
destinationPath: context.testFileResolved,
|
|
63
|
+
templateType: 'test',
|
|
64
|
+
language: context.language,
|
|
65
|
+
preventOverwrite: context.preventOverwrite,
|
|
66
|
+
templateContext: context.templateContext,
|
|
67
|
+
});
|
|
92
68
|
this.stopSpinner();
|
|
93
69
|
|
|
94
70
|
// * rewire the index.js to point to the new file
|
|
95
|
-
this.startSpinner(`Rewriting your ${
|
|
71
|
+
this.startSpinner(`Rewriting your ${context.indexFileLocal}`);
|
|
96
72
|
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
templateContext.VARIABLE,
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
templateContext.KEY
|
|
105
|
-
);
|
|
73
|
+
const originalContents = await updateEntryFile({
|
|
74
|
+
language: context.language,
|
|
75
|
+
indexFileResolved: context.indexFileResolved,
|
|
76
|
+
actionRelativeImportPath: context.actionRelativeImportPath,
|
|
77
|
+
actionImportName: context.templateContext.VARIABLE,
|
|
78
|
+
actionType: context.actionType,
|
|
79
|
+
});
|
|
106
80
|
|
|
107
81
|
if (isValidAppInstall().valid) {
|
|
108
82
|
const success = isValidEntryFileUpdate(
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
83
|
+
context.language,
|
|
84
|
+
context.indexFileResolved,
|
|
85
|
+
context.actionType,
|
|
86
|
+
context.templateContext.KEY
|
|
112
87
|
);
|
|
113
88
|
|
|
114
89
|
this.stopSpinner({ success });
|
|
115
90
|
|
|
116
91
|
if (!success) {
|
|
117
|
-
const entryName = splitFileFromPath(
|
|
92
|
+
const entryName = splitFileFromPath(context.indexFileResolved)[1];
|
|
118
93
|
|
|
119
94
|
this.startSpinner(
|
|
120
95
|
`Unable to successfully rewrite your ${entryName}. Rolling back...`
|
|
121
96
|
);
|
|
122
|
-
await writeFile(
|
|
97
|
+
await writeFile(context.indexFileResolved, originalContents);
|
|
123
98
|
this.stopSpinner();
|
|
124
99
|
|
|
125
100
|
this.error(
|
|
126
101
|
[
|
|
127
|
-
`\nPlease add the following lines to ${
|
|
128
|
-
` * \`const ${
|
|
129
|
-
|
|
130
|
-
} = require('./${getRelativeRequirePath(
|
|
131
|
-
entryFilePath,
|
|
132
|
-
getFullActionFilePath(newActionDir, actionKey)
|
|
133
|
-
)}');\` at the top-level`,
|
|
134
|
-
` * \`[${templateContext.VARIABLE}.key]: ${
|
|
135
|
-
templateContext.VARIABLE
|
|
136
|
-
}\` in the "${plural(
|
|
137
|
-
actionType
|
|
138
|
-
)}" object in your exported integration definition.`,
|
|
102
|
+
`\nPlease add the following lines to ${context.indexFileResolved}:`,
|
|
103
|
+
` * \`const ${context.templateContext.VARIABLE} = require('./${context.actionRelativeImportPath}');\` at the top-level`,
|
|
104
|
+
` * \`[${context.templateContext.VARIABLE}.key]: ${context.templateContext.VARIABLE}\` in the "${context.actionTypePlural}" object in your exported integration definition.`,
|
|
139
105
|
'',
|
|
140
|
-
`Also, please file an issue at ${ISSUES_URL} with the contents of your ${
|
|
106
|
+
`Also, please file an issue at ${ISSUES_URL} with the contents of your ${context.indexFileResolved}.`,
|
|
141
107
|
].join('\n')
|
|
142
108
|
);
|
|
143
109
|
}
|
|
@@ -146,8 +112,51 @@ class ScaffoldCommand extends BaseCommand {
|
|
|
146
112
|
this.stopSpinner();
|
|
147
113
|
|
|
148
114
|
if (!this.flags.invokedFromAnotherCommand) {
|
|
149
|
-
this.log(`\nAll done! Your new ${actionType} is ready to use.`);
|
|
115
|
+
this.log(`\nAll done! Your new ${context.actionType} is ready to use.`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* If `--entry` is not provided, this will determine the path to the
|
|
121
|
+
* root index file. Notably, we'll look for tsconfig.json and
|
|
122
|
+
* src/index.ts first, because even TS apps have a root level plain
|
|
123
|
+
* index.js that we should ignore.
|
|
124
|
+
*
|
|
125
|
+
* @returns {string}
|
|
126
|
+
*/
|
|
127
|
+
defaultIndexFileLocal() {
|
|
128
|
+
const tsConfigPath = path.join(process.cwd(), 'tsconfig.json');
|
|
129
|
+
const srcIndexTsPath = path.join(process.cwd(), 'src', 'index.ts');
|
|
130
|
+
if (fs.existsSync(tsConfigPath) && fs.existsSync(srcIndexTsPath)) {
|
|
131
|
+
this.log('Automatically detected TypeScript project');
|
|
132
|
+
return 'src/index.ts';
|
|
150
133
|
}
|
|
134
|
+
|
|
135
|
+
return 'index.js';
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* If `--dest` is not provided, this will determine the directory for
|
|
140
|
+
* the new action file to be created in.
|
|
141
|
+
*
|
|
142
|
+
* @param {string} indexFileLocal - The path to the index file
|
|
143
|
+
* @returns {string}
|
|
144
|
+
*/
|
|
145
|
+
defaultActionDirLocal(indexFileLocal) {
|
|
146
|
+
const parent = path.dirname(indexFileLocal);
|
|
147
|
+
return path.join(parent, plural(this.args.actionType));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* If `--test-dest` is not provided, this will determine the directory
|
|
152
|
+
* for the new test file to be created in.
|
|
153
|
+
*
|
|
154
|
+
* @param {string} indexFileLocal - The path to the index file
|
|
155
|
+
* @returns {string}
|
|
156
|
+
*/
|
|
157
|
+
defaultTestDirLocal(indexFileLocal) {
|
|
158
|
+
const parent = path.dirname(indexFileLocal);
|
|
159
|
+
return path.join(parent, 'test', plural(this.args.actionType));
|
|
151
160
|
}
|
|
152
161
|
}
|
|
153
162
|
|
|
@@ -179,13 +188,12 @@ ScaffoldCommand.flags = buildFlags({
|
|
|
179
188
|
entry: flags.string({
|
|
180
189
|
char: 'e',
|
|
181
190
|
description:
|
|
182
|
-
"Supply the path to your integration's
|
|
183
|
-
default: 'index.js',
|
|
191
|
+
"Supply the path to your integration's entry point (`index.js` or `src/index.ts`). This will try to automatically detect the correct file if not provided.",
|
|
184
192
|
}),
|
|
185
193
|
force: flags.boolean({
|
|
186
194
|
char: 'f',
|
|
187
195
|
description:
|
|
188
|
-
'Should we overwrite an
|
|
196
|
+
'Should we overwrite an existing trigger/search/create file?',
|
|
189
197
|
default: false,
|
|
190
198
|
}),
|
|
191
199
|
'no-help': flags.boolean({
|
package/src/utils/ast.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
//
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
// tools for modifying an AST
|
|
2
4
|
|
|
3
5
|
const j = require('jscodeshift');
|
|
6
|
+
const ts = j.withParser('ts');
|
|
4
7
|
|
|
5
8
|
// simple helper functions used for searching for nodes
|
|
6
9
|
// can't use j.identifier(name) because it has extra properties and we have to have no extras to find nodes
|
|
@@ -21,7 +24,7 @@ const typeHelpers = {
|
|
|
21
24
|
/**
|
|
22
25
|
* adds a `const verName = require(path)` to the root of a codeStr
|
|
23
26
|
*/
|
|
24
|
-
const
|
|
27
|
+
const importActionInJsApp = (codeStr, varName, path) => {
|
|
25
28
|
if (codeStr.match(new RegExp(`${varName} ?= ?require`))) {
|
|
26
29
|
// duplicate identifier, no need to re-add
|
|
27
30
|
// this would fail if they used this variable name for something else; we'd keep going and double-declare that variable
|
|
@@ -61,7 +64,7 @@ const createRootRequire = (codeStr, varName, path) => {
|
|
|
61
64
|
return root.toSource();
|
|
62
65
|
};
|
|
63
66
|
|
|
64
|
-
const
|
|
67
|
+
const registerActionInJsApp = (codeStr, property, varName) => {
|
|
65
68
|
// to play with this, use https://astexplorer.net/#/gist/cb4986b3f1c6eb975339608109a48e7d/0fbf2fabbcf27d0b6ebd8910f979bd5d97dd9404
|
|
66
69
|
|
|
67
70
|
const root = j(codeStr);
|
|
@@ -136,4 +139,102 @@ const addKeyToPropertyOnApp = (codeStr, property, varName) => {
|
|
|
136
139
|
return root.toSource();
|
|
137
140
|
};
|
|
138
141
|
|
|
139
|
-
|
|
142
|
+
/**
|
|
143
|
+
* Adds an import statement to the top of an index.ts file to import a
|
|
144
|
+
* new action, such as `import some_trigger from './triggers/some_trigger';`
|
|
145
|
+
*
|
|
146
|
+
* @param {string} codeStr - The code of the index.ts file to modify.
|
|
147
|
+
* @param {string} identifierName - The name of imported action used as a variable in the code.
|
|
148
|
+
* @param {string} actionRelativeImportPath - The relative path to import the action from
|
|
149
|
+
* @returns {string}
|
|
150
|
+
*/
|
|
151
|
+
const importActionInTsApp = (
|
|
152
|
+
codeStr,
|
|
153
|
+
identifierName,
|
|
154
|
+
actionRelativeImportPath
|
|
155
|
+
) => {
|
|
156
|
+
const root = ts(codeStr);
|
|
157
|
+
|
|
158
|
+
const imports = root.find(ts.ImportDeclaration);
|
|
159
|
+
|
|
160
|
+
const newImportStatement = j.importDeclaration(
|
|
161
|
+
[j.importDefaultSpecifier(j.identifier(identifierName))],
|
|
162
|
+
j.literal(actionRelativeImportPath)
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
if (imports.length) {
|
|
166
|
+
imports.at(-1).insertAfter(newImportStatement);
|
|
167
|
+
} else {
|
|
168
|
+
const body = root.find(ts.Program).get().node.body;
|
|
169
|
+
body.unshift(newImportStatement);
|
|
170
|
+
// Add newline after import?
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return root.toSource({ quote: 'single' });
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
*
|
|
178
|
+
* @param {string} codeStr
|
|
179
|
+
* @param {'creates' | 'searches' | 'triggers'} actionTypePlural - The type of action to register within the app
|
|
180
|
+
* @param {string} identifierName - Name of the action imported to be registered
|
|
181
|
+
* @returns {string}
|
|
182
|
+
*/
|
|
183
|
+
const registerActionInTsApp = (codeStr, actionTypePlural, identifierName) => {
|
|
184
|
+
const root = ts(codeStr);
|
|
185
|
+
|
|
186
|
+
// the `[thing.key]: thing` entry we'd like to insert.
|
|
187
|
+
const newProperty = ts.property.from({
|
|
188
|
+
kind: 'init',
|
|
189
|
+
key: j.memberExpression(j.identifier(identifierName), j.identifier('key')),
|
|
190
|
+
value: j.identifier(identifierName),
|
|
191
|
+
computed: true,
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// Find the top level app Object; the one with the `platformVersion`
|
|
195
|
+
// key. This is where we'll insert our new property.
|
|
196
|
+
const appObjectCandidates = root
|
|
197
|
+
.find(ts.ObjectExpression)
|
|
198
|
+
.filter((path) =>
|
|
199
|
+
path.value.properties.some(
|
|
200
|
+
(prop) => prop.key && prop.key.name === 'platformVersion'
|
|
201
|
+
)
|
|
202
|
+
);
|
|
203
|
+
if (appObjectCandidates.length !== 1) {
|
|
204
|
+
throw new Error('Unable to find the app definition to modify');
|
|
205
|
+
}
|
|
206
|
+
const appObj = appObjectCandidates.get().node;
|
|
207
|
+
|
|
208
|
+
// Now we have an app object to modify.
|
|
209
|
+
|
|
210
|
+
// Check if this object already has the actionType group inside it.
|
|
211
|
+
const existingProp = appObj.properties.find(
|
|
212
|
+
(props) => props.key.name === actionTypePlural
|
|
213
|
+
);
|
|
214
|
+
if (existingProp) {
|
|
215
|
+
const value = existingProp.value;
|
|
216
|
+
if (value.type !== 'ObjectExpression') {
|
|
217
|
+
throw new Error(
|
|
218
|
+
`Tried to edit the ${actionTypePlural} key, but the value wasn't an object`
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
value.properties.push(newProperty);
|
|
222
|
+
} else {
|
|
223
|
+
appObj.properties.push(
|
|
224
|
+
j.property(
|
|
225
|
+
'init',
|
|
226
|
+
j.identifier(actionTypePlural),
|
|
227
|
+
j.objectExpression([newProperty])
|
|
228
|
+
)
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return root.toSource({ quote: 'single' });
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
module.exports = {
|
|
236
|
+
importActionInJsApp,
|
|
237
|
+
registerActionInJsApp,
|
|
238
|
+
importActionInTsApp,
|
|
239
|
+
registerActionInTsApp,
|
|
240
|
+
};
|