zapier-platform-cli 17.3.0 → 17.4.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.
Files changed (30) hide show
  1. package/oclif.manifest.json +16 -4
  2. package/package.json +1 -1
  3. package/scaffold/resource.template.ts +30 -9
  4. package/src/generators/index.js +70 -68
  5. package/src/generators/templates/README.template.md +10 -0
  6. package/src/generators/templates/authTests/basic.test.ts +42 -0
  7. package/src/generators/templates/authTests/custom.test.ts +34 -0
  8. package/src/generators/templates/authTests/digest.test.ts +43 -0
  9. package/src/generators/templates/authTests/oauth1.test.ts +63 -0
  10. package/src/generators/templates/authTests/oauth2.test.ts +115 -0
  11. package/src/generators/templates/authTests/session.test.ts +36 -0
  12. package/src/generators/templates/index.template.ts +11 -14
  13. package/src/generators/templates/tsconfig.template.json +18 -0
  14. package/src/oclif/commands/init.js +9 -2
  15. package/src/oclif/commands/versions.js +0 -24
  16. package/src/utils/auth-files-codegen.js +141 -69
  17. package/src/utils/build.js +62 -53
  18. package/src/utils/codegen.js +24 -4
  19. package/src/utils/files.js +12 -2
  20. package/src/utils/zapierwrapper.js +1 -1
  21. package/src/generators/templates/index-esm.template.ts +0 -24
  22. package/src/generators/templates/typescript/README.md +0 -3
  23. package/src/generators/templates/typescript/src/authentication.ts +0 -48
  24. package/src/generators/templates/typescript/src/constants.ts +0 -3
  25. package/src/generators/templates/typescript/src/creates/movie.ts +0 -43
  26. package/src/generators/templates/typescript/src/middleware.ts +0 -11
  27. package/src/generators/templates/typescript/src/test/creates.test.ts +0 -21
  28. package/src/generators/templates/typescript/src/test/triggers.test.ts +0 -25
  29. package/src/generators/templates/typescript/src/triggers/movie.ts +0 -29
  30. package/src/generators/templates/typescript/tsconfig.json +0 -17
@@ -64,12 +64,10 @@ const findRequiredFiles = async (workingDir, entryPoints) => {
64
64
  platform: 'node',
65
65
  metafile: true,
66
66
  logLevel: 'warning',
67
- external: [
68
- '../test/userapp',
69
- 'zapier-platform-core/src/http-middlewares/before/sanatize-headers', // appears in zapier-platform-legacy-scripting-runner/index.js
70
- './request-worker', // appears in zapier-platform-legacy-scripting-runner/zfactory.js
71
- './xhr-sync-worker.js', // appears in jsdom/living/xmlhttprequest.js
72
- ],
67
+ logOverride: {
68
+ 'require-resolve-not-external': 'silent',
69
+ },
70
+ external: ['../test/userapp'],
73
71
  format,
74
72
  // Setting conditions to an empty array to exclude 'module' condition,
75
73
  // which Node.js doesn't use. https://esbuild.github.io/api/#conditions
@@ -220,44 +218,6 @@ const openZip = (outputPath) => {
220
218
  return zip;
221
219
  };
222
220
 
223
- const looksLikeWorkspaceRoot = async (dir) => {
224
- if (fs.existsSync(path.join(dir, 'pnpm-workspace.yaml'))) {
225
- return true;
226
- }
227
-
228
- const packageJsonPath = path.join(dir, 'package.json');
229
- if (!fs.existsSync(packageJsonPath)) {
230
- return false;
231
- }
232
-
233
- let packageJson;
234
- try {
235
- packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
236
- } catch (err) {
237
- return false;
238
- }
239
-
240
- return packageJson?.workspaces != null;
241
- };
242
-
243
- // Traverses up the directory tree to find the workspace root. The workspace
244
- // root directory either contains pnpm-workspace.yaml or a package.json file
245
- // with a "workspaces" field. Returns the absolute path to the workspace root
246
- // directory, or null if not found.
247
- const findWorkspaceRoot = async (workingDir) => {
248
- let dir = workingDir;
249
- for (let i = 0; i < 500; i++) {
250
- if (await looksLikeWorkspaceRoot(dir)) {
251
- return dir;
252
- }
253
- if (dir === '/' || dir.match(/^[a-z]:\\$/i)) {
254
- break;
255
- }
256
- dir = path.dirname(dir);
257
- }
258
- return null;
259
- };
260
-
261
221
  const getNearestNodeModulesDir = (workingDir, relPath) => {
262
222
  if (path.basename(relPath) === 'package.json') {
263
223
  const nmDir = path.resolve(
@@ -266,7 +226,7 @@ const getNearestNodeModulesDir = (workingDir, relPath) => {
266
226
  'node_modules',
267
227
  );
268
228
  return fs.existsSync(nmDir) ? path.relative(workingDir, nmDir) : null;
269
- } else {
229
+ } else if (relPath.includes('node_modules')) {
270
230
  let dir = path.dirname(relPath);
271
231
  for (let i = 0; i < 100; i++) {
272
232
  if (dir.endsWith(`${path.sep}node_modules`)) {
@@ -278,8 +238,49 @@ const getNearestNodeModulesDir = (workingDir, relPath) => {
278
238
  }
279
239
  dir = nextDir;
280
240
  }
281
- return null;
282
241
  }
242
+
243
+ let dir = path.dirname(relPath);
244
+ for (let i = 0; i < 100; i++) {
245
+ const nmDir = path.join(dir, 'node_modules');
246
+ if (fs.existsSync(path.resolve(workingDir, nmDir))) {
247
+ return nmDir;
248
+ }
249
+ const nextDir = path.dirname(dir);
250
+ if (nextDir === dir) {
251
+ break;
252
+ }
253
+ dir = nextDir;
254
+ }
255
+
256
+ return null;
257
+ };
258
+
259
+ const countLeadingDoubleDots = (relPath) => {
260
+ const parts = relPath.split(path.sep);
261
+ for (let i = 0; i < parts.length; i++) {
262
+ if (parts[i] !== '..') {
263
+ return i;
264
+ }
265
+ }
266
+ return 0;
267
+ };
268
+
269
+ // Join all relPaths with workingDir and return the common ancestor directory.
270
+ const findCommonAncestor = (workingDir, relPaths) => {
271
+ let maxLeadingDoubleDots = 0;
272
+ for (const relPath of relPaths) {
273
+ maxLeadingDoubleDots = Math.max(
274
+ maxLeadingDoubleDots,
275
+ countLeadingDoubleDots(relPath),
276
+ );
277
+ }
278
+
279
+ let commonAncestor = workingDir;
280
+ for (let i = 0; i < maxLeadingDoubleDots; i++) {
281
+ commonAncestor = path.dirname(commonAncestor);
282
+ }
283
+ return commonAncestor;
283
284
  };
284
285
 
285
286
  const writeBuildZipDumbly = async (workingDir, zip) => {
@@ -322,11 +323,14 @@ const writeBuildZipSmartly = async (workingDir, zip) => {
322
323
  ]),
323
324
  ).sort();
324
325
 
325
- const workspaceRoot = (await findWorkspaceRoot(workingDir)) || workingDir;
326
+ const zipRoot = findCommonAncestor(workingDir, relPaths) || workingDir;
326
327
 
327
- if (workspaceRoot !== workingDir) {
328
- const appDirRelPath = path.relative(workspaceRoot, workingDir);
329
- const linkNames = ['zapierwrapper.js', 'index.js'];
328
+ if (zipRoot !== workingDir) {
329
+ const appDirRelPath = path.relative(zipRoot, workingDir);
330
+ // zapierwrapper.js and index.js are entry points.
331
+ // 'config' is the default directory that the 'config' npm package expects
332
+ // to find config files at the root directory.
333
+ const linkNames = ['zapierwrapper.js', 'index.js', 'config'];
330
334
  for (const name of linkNames) {
331
335
  if (fs.existsSync(path.join(workingDir, name))) {
332
336
  zip.symlink(name, path.join(appDirRelPath, name), 0o644);
@@ -343,8 +347,8 @@ const writeBuildZipSmartly = async (workingDir, zip) => {
343
347
  // Write required files to the zip
344
348
  for (const relPath of relPaths) {
345
349
  const absPath = path.resolve(workingDir, relPath);
346
- const nameInZip = path.relative(workspaceRoot, absPath);
347
- if (nameInZip === 'package.json' && workspaceRoot !== workingDir) {
350
+ const nameInZip = path.relative(zipRoot, absPath);
351
+ if (nameInZip === 'package.json' && zipRoot !== workingDir) {
348
352
  // Ignore workspace root's package.json
349
353
  continue;
350
354
  }
@@ -381,7 +385,7 @@ const writeBuildZipSmartly = async (workingDir, zip) => {
381
385
  symlink.parentPath,
382
386
  symlink.name,
383
387
  );
384
- const nameInZip = path.relative(workspaceRoot, absPath);
388
+ const nameInZip = path.relative(zipRoot, absPath);
385
389
  const targetInZip = path.relative(
386
390
  symlink.parentPath,
387
391
  fs.realpathSync(absPath),
@@ -548,6 +552,11 @@ const testBuildZip = async (zipPath) => {
548
552
  `You may have to add it to ${colors.bold.underline('includeInBuild')} ` +
549
553
  `in your ${colors.bold.underline('.zapierapprc')} file.`,
550
554
  );
555
+ } else if (error.message) {
556
+ // Hide the unzipped temporary directory
557
+ error.message = error.message
558
+ .replaceAll(`file://${testDir}/`, '')
559
+ .replaceAll(`${testDir}/`, '');
551
560
  }
552
561
  throw error;
553
562
  }
@@ -23,19 +23,30 @@ const obj = (...properties) =>
23
23
  .join('')}}
24
24
  `.trim();
25
25
 
26
- const exportStatement = (obj) => `
27
- module.exports = ${obj}`;
26
+ // TypeScript version of obj that supports satisfies clause
27
+ const objTS = (type, ...properties) =>
28
+ `
29
+ {${properties
30
+ .map((p) => (p.startsWith('/') ? `\n\n${p}\n` : p + ','))
31
+ .join('')}}
32
+ `.trim() + ` satisfies ${type}`;
33
+
34
+ const exportStatement = (obj, language) =>
35
+ language === 'typescript'
36
+ ? `export default ${obj}`
37
+ : `module.exports = ${obj}`;
28
38
 
29
39
  /**
30
40
  * @param {string} key could be a variable name or string value
31
41
  * @param {string | undefined} value can either be a variable or actual string. or could be missing, in which case the input is treated as a variable
42
+ * @param {string | undefined} satisfiesType if provided, the property will be typed with the given type
32
43
  */
33
- const objProperty = (key, value) => {
44
+ const objProperty = (key, value, satisfiesType) => {
34
45
  if (value === undefined) {
35
46
  return `${key}`;
36
47
  }
37
48
  // wrap key in quotes here in case the key isn't a valid property. prettier will remove if needed
38
- return `'${key}': ${value}`;
49
+ return `'${key}': ${value}${satisfiesType ? ` satisfies ${satisfiesType}` : ''}`;
39
50
  };
40
51
 
41
52
  const variableAssignmentDeclaration = (varName, value) =>
@@ -157,6 +168,13 @@ const file = (...statements) =>
157
168
  ${statements.join('\n\n')}
158
169
  `.trim();
159
170
 
171
+ const fileTS = (typesToImport = [], ...statements) =>
172
+ `
173
+ import type { ${typesToImport.join(', ')} } from 'zapier-platform-core';
174
+
175
+ ${statements.join('\n\n')}
176
+ `.trim();
177
+
160
178
  module.exports = {
161
179
  arr,
162
180
  assignmentStatement,
@@ -166,11 +184,13 @@ module.exports = {
166
184
  exportStatement,
167
185
  fatArrowReturnFunctionDeclaration,
168
186
  file,
187
+ fileTS,
169
188
  functionDeclaration,
170
189
  ifStatement,
171
190
  interpLiteral,
172
191
  obj,
173
192
  objProperty,
193
+ objTS,
174
194
  RESPONSE_VAR,
175
195
  returnStatement,
176
196
  strLiteral,
@@ -207,9 +207,14 @@ function* walkDir(dir) {
207
207
  const entries = fs.readdirSync(dir, { withFileTypes: true });
208
208
  for (const entry of entries) {
209
209
  if (entry.isDirectory()) {
210
- const subDir = path.join(entry.parentPath, entry.name);
210
+ const subDir = path.join(dir, entry.name);
211
211
  yield* walkDir(subDir);
212
212
  } else if (entry.isFile() || entry.isSymbolicLink()) {
213
+ if (!entry.parentPath) {
214
+ // dirent.parentPath is only available since Node.js 18.20, 20.12, and
215
+ // 21.4. Re-assigning it so the caller can use dirent.parentPath.
216
+ entry.parentPath = dir;
217
+ }
213
218
  yield entry;
214
219
  }
215
220
  }
@@ -223,10 +228,15 @@ function* walkDirLimitedLevels(dir, maxLevels, currentLevel = 1) {
223
228
  for (const entry of entries) {
224
229
  if (entry.isDirectory()) {
225
230
  if (currentLevel < maxLevels) {
226
- const subDir = path.join(entry.parentPath, entry.name);
231
+ const subDir = path.join(dir, entry.name);
227
232
  yield* walkDirLimitedLevels(subDir, maxLevels, currentLevel + 1);
228
233
  }
229
234
  } else if (entry.isFile() || entry.isSymbolicLink()) {
235
+ if (!entry.parentPath) {
236
+ // dirent.parentPath is only available since Node.js 18.20, 20.12, and
237
+ // 21.4. Re-assigning it so the caller can use dirent.parentPath.
238
+ entry.parentPath = dir;
239
+ }
230
240
  yield entry;
231
241
  }
232
242
  }
@@ -31,7 +31,7 @@ const copyZapierWrapper = async (corePackageDir, appDir) => {
31
31
  );
32
32
  }
33
33
 
34
- const wrapperText = (await readFile(wrapperPath, 'utf8')).replace(
34
+ const wrapperText = (await readFile(wrapperPath, 'utf8')).replaceAll(
35
35
  '{REPLACE_ME_PACKAGE_NAME}',
36
36
  appPackageJson.name,
37
37
  );
@@ -1,24 +0,0 @@
1
- import zapier, { defineApp } from 'zapier-platform-core';
2
-
3
- import packageJson from '../package.json' with { type: 'json' };
4
-
5
- import MovieCreate from './creates/movie.js';
6
- import MovieTrigger from './triggers/movie.js';
7
- import authentication from './authentication.js';
8
- import { addBearerHeader } from './middleware.js';
9
-
10
- export default defineApp({
11
- version: packageJson.version,
12
- platformVersion: zapier.version,
13
-
14
- authentication,
15
- beforeRequest: [addBearerHeader],
16
-
17
- triggers: {
18
- [MovieTrigger.key]: MovieTrigger,
19
- },
20
-
21
- creates: {
22
- [MovieCreate.key]: MovieCreate,
23
- },
24
- });
@@ -1,3 +0,0 @@
1
- # TypeScript Template
2
-
3
- An TypeScript template for Zapier Integrations.
@@ -1,48 +0,0 @@
1
- import type { Authentication } from 'zapier-platform-core';
2
-
3
- import { API_URL, SCOPES } from './constants.js';
4
-
5
- export default {
6
- type: 'oauth2',
7
- test: { url: `${API_URL}/me` },
8
- connectionLabel: '{{email}}', // Set this from the test data.
9
- oauth2Config: {
10
- authorizeUrl: {
11
- url: `${API_URL}/oauth/authorize`,
12
- params: {
13
- client_id: '{{process.env.CLIENT_ID}}',
14
- response_type: 'code',
15
- scope: SCOPES.join(' '),
16
- redirect_uri: '{{bundle.inputData.redirect_uri}}',
17
- state: '{{bundle.inputData.state}}',
18
- },
19
- },
20
- getAccessToken: {
21
- url: `${API_URL}/oauth/access-token`,
22
- method: 'POST',
23
- headers: {
24
- 'content-type': 'application/x-www-form-urlencoded',
25
- },
26
- body: {
27
- client_id: '{{process.env.CLIENT_ID}}',
28
- client_secret: '{{process.env.CLIENT_SECRET}}',
29
- code: '{{bundle.inputData.code}}',
30
- grant_type: 'authorization_code',
31
- redirect_uri: '{{bundle.inputData.redirect_uri}}',
32
- },
33
- },
34
- refreshAccessToken: {
35
- url: `${API_URL}/oauth/refresh-token`,
36
- method: 'POST',
37
- headers: {
38
- 'content-type': 'application/x-www-form-urlencoded',
39
- },
40
- body: {
41
- client_id: '{{process.env.CLIENT_ID}}',
42
- client_secret: '{{process.env.CLIENT_SECRET}}',
43
- refresh_token: '{{bundle.authData.refresh_token}}',
44
- grant_type: 'refresh_token',
45
- },
46
- },
47
- },
48
- } satisfies Authentication;
@@ -1,3 +0,0 @@
1
- export const API_URL = 'https://auth-json-server.zapier-staging.com';
2
-
3
- export const SCOPES = ['movies:read', 'movies:write'];
@@ -1,43 +0,0 @@
1
- import {
2
- defineInputFields,
3
- defineCreate,
4
- type CreatePerform,
5
- type InferInputData,
6
- } from 'zapier-platform-core';
7
- import { API_URL } from '../constants.js';
8
-
9
- const inputFields = defineInputFields([
10
- { key: 'title', required: true },
11
- { key: 'year', type: 'integer' },
12
- ]);
13
-
14
- const perform = (async (z, bundle) => {
15
- const response = await z.request({
16
- method: 'POST',
17
- url: `${API_URL}/movies`,
18
- body: {
19
- title: bundle.inputData.title,
20
- year: bundle.inputData.year,
21
- },
22
- });
23
- return response.data;
24
- }) satisfies CreatePerform<InferInputData<typeof inputFields>>;
25
-
26
- export default defineCreate({
27
- key: 'movie',
28
- noun: 'Movie',
29
-
30
- display: {
31
- label: 'Create Movie',
32
- description: 'Creates a new movie.',
33
- },
34
-
35
- operation: {
36
- perform,
37
- inputFields,
38
- sample: {
39
- id: '1',
40
- title: 'example',
41
- },
42
- },
43
- });
@@ -1,11 +0,0 @@
1
- import type { BeforeRequestMiddleware } from 'zapier-platform-core';
2
-
3
- export const addBearerHeader: BeforeRequestMiddleware = (request, z, bundle) => {
4
- if (bundle.authData.access_token && !request.headers?.Authorization) {
5
- request.headers = {
6
- ...request.headers,
7
- Authorization: `Bearer ${bundle.authData.access_token}`,
8
- }
9
- }
10
- return request;
11
- };
@@ -1,21 +0,0 @@
1
- import { createAppTester, tools } from 'zapier-platform-core';
2
- import { describe, test, expect } from 'vitest';
3
-
4
- import App from '../index';
5
-
6
- const appTester = createAppTester(App);
7
- tools.env.inject();
8
-
9
- describe('movie', () => {
10
- test('create a movie', async () => {
11
- const bundle = {
12
- inputData: { title: 'hello', year: 2020 },
13
- authData: { access_token: 'a_token' },
14
- };
15
- const result = await appTester(App.creates.movie.operation.perform, bundle);
16
- expect(result).toMatchObject({
17
- title: 'hello',
18
- year: 2020,
19
- });
20
- });
21
- });
@@ -1,25 +0,0 @@
1
- import { createAppTester, tools } from 'zapier-platform-core';
2
- import { describe, test, expect } from 'vitest';
3
-
4
- import App from '../index';
5
-
6
- const appTester = createAppTester(App);
7
- tools.env.inject();
8
-
9
- describe('movie', () => {
10
- test('list movies', async () => {
11
- const bundle = { inputData: {}, authData: { access_token: 'a_token' } };
12
- const results = (await appTester(
13
- App.triggers.movie.operation.perform,
14
- bundle
15
- )) as Array<object>;
16
-
17
- expect(results.length).toBeGreaterThan(0);
18
-
19
- const firstMovie = results[0];
20
- expect(firstMovie).toMatchObject({
21
- id: '1',
22
- title: 'title 1',
23
- });
24
- });
25
- });
@@ -1,29 +0,0 @@
1
- import {
2
- defineTrigger,
3
- type PollingTriggerPerform,
4
- } from 'zapier-platform-core';
5
- import { API_URL } from '../constants.js';
6
-
7
- const perform = (async (z, bundle) => {
8
- const response = await z.request(`${API_URL}/movies`);
9
- return response.data;
10
- }) satisfies PollingTriggerPerform;
11
-
12
- export default defineTrigger({
13
- key: 'movie',
14
- noun: 'Movie',
15
-
16
- display: {
17
- label: 'New Movie',
18
- description: 'Triggers when a new movie is created.',
19
- },
20
-
21
- operation: {
22
- type: 'polling',
23
- perform,
24
- sample: {
25
- id: '1',
26
- title: 'example',
27
- },
28
- },
29
- });
@@ -1,17 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ESNext",
4
- "module": "NodeNext",
5
- "moduleResolution": "NodeNext",
6
- "resolveJsonModule": true,
7
- "esModuleInterop": true,
8
- "noUncheckedIndexedAccess": true,
9
- "isolatedModules": true,
10
- "skipLibCheck": true,
11
- "outDir": "./dist",
12
- "rootDir": "./src",
13
- "strict": true
14
- },
15
- "include": ["./src/**/*.ts"],
16
- "exclude": ["./**/*.test.ts"]
17
- }