zapier-platform-cli 17.1.0 → 17.3.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.
@@ -1,31 +1,34 @@
1
- const crypto = require('crypto');
2
- const os = require('os');
3
- const path = require('path');
1
+ const crypto = require('node:crypto');
2
+ const fs = require('node:fs');
3
+ const os = require('node:os');
4
+ const path = require('node:path');
5
+ const {
6
+ constants: { Z_BEST_COMPRESSION },
7
+ } = require('node:zlib');
4
8
 
5
9
  const _ = require('lodash');
6
10
  const archiver = require('archiver');
11
+ const colors = require('colors/safe');
7
12
  const esbuild = require('esbuild');
8
- const fs = require('fs');
9
13
  const fse = require('fs-extra');
10
- const klaw = require('klaw');
11
14
  const updateNotifier = require('update-notifier');
12
- const colors = require('colors/safe');
13
- const semver = require('semver');
14
- const { minimatch } = require('minimatch');
15
+ const decompress = require('decompress');
15
16
 
16
17
  const {
17
- constants: { Z_BEST_COMPRESSION },
18
- } = require('zlib');
19
-
20
- const constants = require('../constants');
21
-
22
- const { writeFile, copyDir, ensureDir, removeDir } = require('./files');
18
+ BUILD_DIR,
19
+ BUILD_PATH,
20
+ PLATFORM_PACKAGE,
21
+ SOURCE_PATH,
22
+ UPDATE_NOTIFICATION_INTERVAL,
23
+ } = require('../constants');
24
+ const { copyDir, walkDir, walkDirLimitedLevels } = require('./files');
25
+ const { iterfilter, itermap } = require('./itertools');
23
26
 
24
27
  const {
25
- prettyJSONstringify,
26
- startSpinner,
27
28
  endSpinner,
28
29
  flattenCheckResult,
30
+ prettyJSONstringify,
31
+ startSpinner,
29
32
  } = require('./display');
30
33
 
31
34
  const {
@@ -35,33 +38,23 @@ const {
35
38
  validateApp,
36
39
  } = require('./api');
37
40
 
38
- const { copyZapierWrapper } = require('./zapierwrapper');
41
+ const { copyZapierWrapper, deleteZapierWrapper } = require('./zapierwrapper');
39
42
 
40
43
  const checkMissingAppInfo = require('./check-missing-app-info');
41
44
 
42
- const { runCommand, isWindows, findCorePackageDir } = require('./misc');
43
- const { respectGitIgnore } = require('./ignore');
45
+ const { findCorePackageDir, isWindows, runCommand } = require('./misc');
46
+ const { isBlocklisted, respectGitIgnore } = require('./ignore');
44
47
  const { localAppCommand } = require('./local');
45
48
 
46
49
  const debug = require('debug')('zapier:build');
47
50
 
48
- const stripPath = (cwd, filePath) => filePath.split(cwd).pop();
51
+ // const stripPath = (cwd, filePath) => filePath.split(cwd).pop();
49
52
 
50
- // given entry points in a directory, return a list of files that uses
51
- const requiredFiles = async ({ cwd, entryPoints }) => {
52
- if (!_.endsWith(cwd, path.sep)) {
53
- cwd += path.sep;
54
- }
55
-
56
- const appPackageJson = require(path.join(cwd, 'package.json'));
53
+ // Given entry points in a directory, return an array of file paths that are
54
+ // required for the build. The returned paths are relative to workingDir.
55
+ const findRequiredFiles = async (workingDir, entryPoints) => {
56
+ const appPackageJson = require(path.join(workingDir, 'package.json'));
57
57
  const isESM = appPackageJson.type === 'module';
58
- // Only include 'module' condition if the app is an ESM app (PDE-6187)
59
- // otherwise exclude 'module' since it breaks the build for hybrid packages (like uuid)
60
- // in CJS apps by only including ESM version of packages.
61
- // An empty list is necessary because otherwise if `platform: node` is specified,
62
- // the 'module' condition is included by default.
63
- // https://esbuild.github.io/api/#conditions
64
- const conditions = isESM ? ['module'] : [];
65
58
  const format = isESM ? 'esm' : 'cjs';
66
59
 
67
60
  const result = await esbuild.build({
@@ -71,111 +64,240 @@ const requiredFiles = async ({ cwd, entryPoints }) => {
71
64
  platform: 'node',
72
65
  metafile: true,
73
66
  logLevel: 'warning',
74
- external: ['../test/userapp'],
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
+ ],
75
73
  format,
76
- conditions,
74
+ // Setting conditions to an empty array to exclude 'module' condition,
75
+ // which Node.js doesn't use. https://esbuild.github.io/api/#conditions
76
+ conditions: [],
77
77
  write: false, // no need to write outfile
78
- absWorkingDir: cwd,
78
+ absWorkingDir: workingDir,
79
79
  tsconfigRaw: '{}',
80
80
  });
81
81
 
82
- return Object.keys(result.metafile.inputs).map((path) =>
83
- stripPath(cwd, path),
84
- );
82
+ let relPaths = Object.keys(result.metafile.inputs);
83
+ if (path.sep === '\\') {
84
+ // The paths in result.metafile.inputs use forward slashes even on Windows,
85
+ // path.normalize() will convert them to backslashes.
86
+ relPaths = relPaths.map((x) => path.normalize(x));
87
+ }
88
+ return relPaths;
85
89
  };
86
90
 
87
- const listFiles = (dir) => {
88
- const isBlocklisted = (filePath) => {
89
- return constants.BLOCKLISTED_PATHS.find((excluded) => {
90
- return filePath.search(excluded) === 0;
91
- });
92
- };
91
+ // From a file path relative to workingDir, traverse up the directory tree until
92
+ // it finds a directory that looks like a package directory, which either
93
+ // contains a package.json file or whose path matches a pattern like
94
+ // 'node_modules/(@scope/)package-name'.
95
+ // Returns null if no package directory is found.
96
+ const getPackageDir = (workingDir, relPath) => {
97
+ const nm = `node_modules${path.sep}`;
98
+ let i = relPath.lastIndexOf(nm);
99
+ if (i < 0) {
100
+ let dir = path.dirname(relPath);
101
+ for (let j = 0; j < 100; j++) {
102
+ const packageJsonPath = path.resolve(workingDir, dir, 'package.json');
103
+ if (fs.existsSync(packageJsonPath)) {
104
+ return dir;
105
+ }
106
+ const nextDir = path.dirname(dir);
107
+ if (nextDir === dir) {
108
+ break;
109
+ }
110
+ dir = nextDir;
111
+ }
112
+ return null;
113
+ }
93
114
 
94
- return new Promise((resolve, reject) => {
95
- const paths = [];
96
- const cwd = dir + path.sep;
97
- klaw(dir, { preserveSymlinks: true })
98
- .on('data', (item) => {
99
- const strippedPath = stripPath(cwd, item.path);
100
- if (!item.stats.isDirectory() && !isBlocklisted(strippedPath)) {
101
- paths.push(strippedPath);
102
- }
103
- })
104
- .on('error', reject)
105
- .on('end', () => {
106
- paths.sort();
107
- resolve(paths);
108
- });
109
- });
115
+ i += nm.length;
116
+ if (relPath[i] === '@') {
117
+ // For scoped package, e.g. node_modules/@zapier/package-name
118
+ const j = relPath.indexOf(path.sep, i + 1);
119
+ if (j < 0) {
120
+ return null;
121
+ }
122
+ i = j + 1; // skip the next path.sep
123
+ }
124
+ const j = relPath.indexOf(path.sep, i);
125
+ if (j < 0) {
126
+ return null;
127
+ }
128
+ return relPath.substring(0, j);
110
129
  };
111
130
 
112
- const forceIncludeDumbPath = (appConfig, filePath) => {
113
- let matchesConfigInclude = false;
114
- const configIncludePaths = _.get(appConfig, 'includeInBuild', []);
115
- _.each(configIncludePaths, (includePath) => {
116
- if (filePath.match(RegExp(includePath, 'i'))) {
117
- matchesConfigInclude = true;
131
+ function expandRequiredFiles(workingDir, relPaths) {
132
+ const expandedPaths = new Set(relPaths);
133
+ for (const relPath of relPaths) {
134
+ const packageDir = getPackageDir(workingDir, relPath);
135
+ if (packageDir) {
136
+ expandedPaths.add(path.join(packageDir, 'package.json'));
137
+ }
138
+ }
139
+ return expandedPaths;
140
+ }
141
+
142
+ // Yields files and symlinks (as fs.Direnv objects) from a directory
143
+ // recursively, excluding names that are typically not needed in the build,
144
+ // such as .git, .env, build, etc.
145
+ function* walkDirWithPresetBlocklist(dir) {
146
+ const shouldInclude = (entry) => {
147
+ const relPath = path.relative(dir, path.join(entry.parentPath, entry.name));
148
+ return !isBlocklisted(relPath);
149
+ };
150
+ yield* iterfilter(shouldInclude, walkDir(dir));
151
+ }
152
+
153
+ // Yields files and symlinks (as fs.Direnv objects) from a directory recursively
154
+ // that match any of the given or preset regex patterns.
155
+ function* walkDirWithPatterns(dir, patterns) {
156
+ const sep = path.sep.replaceAll('\\', '\\\\'); // escape backslash for regex
157
+ const presetPatterns = [
158
+ `${sep}definition\\.json$`,
159
+ `${sep}package\\.json$`,
160
+ `${sep}aws-sdk${sep}apis${sep}.*\\.json$`,
161
+ ];
162
+ patterns = [...presetPatterns, ...(patterns || [])].map(
163
+ (x) => new RegExp(x, 'i'),
164
+ );
165
+ const shouldInclude = (entry) => {
166
+ const relPath = path.join(entry.parentPath, entry.name);
167
+ if (isBlocklisted(relPath)) {
118
168
  return false;
119
169
  }
120
- return true; // Because of consistent-return
170
+ for (const pattern of patterns) {
171
+ if (pattern.test(relPath)) {
172
+ return true;
173
+ }
174
+ }
175
+ return false;
176
+ };
177
+ yield* iterfilter(shouldInclude, walkDir(dir));
178
+ }
179
+
180
+ // Opens a zip file for writing. Returns an Archiver object.
181
+ const openZip = (outputPath) => {
182
+ const output = fs.createWriteStream(outputPath);
183
+ const zip = archiver('zip', {
184
+ zlib: { level: Z_BEST_COMPRESSION }, // Sets the compression level.
121
185
  });
122
186
 
123
- const nodeMajorVersion = semver.coerce(constants.LAMBDA_VERSION).major;
124
-
125
- return (
126
- filePath.endsWith('package.json') ||
127
- filePath.endsWith('definition.json') ||
128
- // include old async deasync versions so this runs seamlessly across node versions
129
- filePath.endsWith(path.join('bin', 'linux-x64-node-10', 'deasync.node')) ||
130
- filePath.endsWith(path.join('bin', 'linux-x64-node-12', 'deasync.node')) ||
131
- filePath.endsWith(path.join('bin', 'linux-x64-node-14', 'deasync.node')) ||
132
- filePath.endsWith(
133
- // Special, for zapier-platform-legacy-scripting-runner
134
- path.join('bin', `linux-x64-node-${nodeMajorVersion}`, 'deasync.node'),
135
- ) ||
136
- filePath.match(
137
- path.sep === '\\' ? /aws-sdk\\apis\\.*\.json/ : /aws-sdk\/apis\/.*\.json/,
138
- ) ||
139
- matchesConfigInclude
140
- );
187
+ const streamCompletePromise = new Promise((resolve, reject) => {
188
+ output.on('close', resolve);
189
+ zip.on('error', reject);
190
+ });
191
+
192
+ zip.finish = async () => {
193
+ // zip.finalize() doesn't return a promise, so here we create a
194
+ // zip.finish() function so the caller can await it.
195
+ // So callers: Use `await zip.finish()` and avoid zip.finalize().
196
+ zip.finalize();
197
+ await streamCompletePromise;
198
+ };
199
+
200
+ if (path.sep === '\\') {
201
+ // On Windows, patch zip.file() and zip.symlink() so they normalize the path
202
+ // separator to '/' because we're supposed to use '/' in a zip file
203
+ // regardless of the OS platform. Those are the only two methods we're
204
+ // currently using. If you wanted to call other zip.xxx methods, you should
205
+ // patch them here as well.
206
+ const origFileMethod = zip.file;
207
+ zip.file = (filepath, data) => {
208
+ filepath = path.normalize(filepath);
209
+ return origFileMethod.call(zip, filepath, data);
210
+ };
211
+ const origSymlinkMethod = zip.symlink;
212
+ zip.symlink = (name, target, mode) => {
213
+ name = path.normalize(name);
214
+ target = path.normalize(target);
215
+ return origSymlinkMethod.call(zip, name, target, mode);
216
+ };
217
+ }
218
+
219
+ zip.pipe(output);
220
+ return zip;
141
221
  };
142
222
 
143
- const writeZipFromPaths = (dir, zipPath, paths) => {
144
- return new Promise((resolve, reject) => {
145
- const output = fs.createWriteStream(zipPath);
146
- const zip = archiver('zip', {
147
- zlib: { level: Z_BEST_COMPRESSION },
148
- });
223
+ const looksLikeWorkspaceRoot = async (dir) => {
224
+ if (fs.existsSync(path.join(dir, 'pnpm-workspace.yaml'))) {
225
+ return true;
226
+ }
149
227
 
150
- // listen for all archive data to be written
151
- output.on('close', function () {
152
- resolve();
153
- });
228
+ const packageJsonPath = path.join(dir, 'package.json');
229
+ if (!fs.existsSync(packageJsonPath)) {
230
+ return false;
231
+ }
154
232
 
155
- zip.on('error', function (err) {
156
- reject(err);
157
- });
233
+ let packageJson;
234
+ try {
235
+ packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
236
+ } catch (err) {
237
+ return false;
238
+ }
158
239
 
159
- // pipe archive data to the file
160
- zip.pipe(output);
240
+ return packageJson?.workspaces != null;
241
+ };
161
242
 
162
- paths.forEach(function (filePath) {
163
- let basePath = path.dirname(filePath);
164
- if (basePath === '.') {
165
- basePath = undefined;
166
- }
167
- const name = path.join(dir, filePath);
168
- zip.file(name, { name: filePath, mode: 0o755 });
169
- });
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
+ };
170
260
 
171
- zip.finalize();
172
- });
261
+ const getNearestNodeModulesDir = (workingDir, relPath) => {
262
+ if (path.basename(relPath) === 'package.json') {
263
+ const nmDir = path.resolve(
264
+ workingDir,
265
+ path.dirname(relPath),
266
+ 'node_modules',
267
+ );
268
+ return fs.existsSync(nmDir) ? path.relative(workingDir, nmDir) : null;
269
+ } else {
270
+ let dir = path.dirname(relPath);
271
+ for (let i = 0; i < 100; i++) {
272
+ if (dir.endsWith(`${path.sep}node_modules`)) {
273
+ return dir;
274
+ }
275
+ const nextDir = path.dirname(dir);
276
+ if (nextDir === dir) {
277
+ break;
278
+ }
279
+ dir = nextDir;
280
+ }
281
+ return null;
282
+ }
173
283
  };
174
284
 
175
- const makeZip = async (dir, zipPath, disableDependencyDetection) => {
176
- const entryPoints = [path.resolve(dir, 'zapierwrapper.js')];
285
+ const writeBuildZipDumbly = async (workingDir, zip) => {
286
+ for (const entry of walkDirWithPresetBlocklist(workingDir)) {
287
+ const absPath = path.resolve(entry.parentPath, entry.name);
288
+ const relPath = path.relative(workingDir, absPath);
289
+ if (entry.isFile()) {
290
+ zip.file(absPath, { name: relPath });
291
+ } else if (entry.isSymbolicLink()) {
292
+ const target = path.relative(entry.parentPath, fs.realpathSync(absPath));
293
+ zip.symlink(relPath, target, 0o644);
294
+ }
295
+ }
296
+ };
177
297
 
178
- const indexPath = path.resolve(dir, 'index.js');
298
+ const writeBuildZipSmartly = async (workingDir, zip) => {
299
+ const entryPoints = [path.resolve(workingDir, 'zapierwrapper.js')];
300
+ const indexPath = path.resolve(workingDir, 'index.js');
179
301
  if (fs.existsSync(indexPath)) {
180
302
  // Necessary for CommonJS integrations. The zapierwrapper they use require()
181
303
  // the index.js file using a variable. esbuild can't detect it, so we need
@@ -183,39 +305,140 @@ const makeZip = async (dir, zipPath, disableDependencyDetection) => {
183
305
  entryPoints.push(indexPath);
184
306
  }
185
307
 
186
- let paths;
308
+ const appConfig = await getLinkedAppConfig(workingDir, false);
309
+ const relPaths = Array.from(
310
+ new Set([
311
+ // Files found by esbuild and their package.json files
312
+ ...expandRequiredFiles(
313
+ workingDir,
314
+ await findRequiredFiles(workingDir, entryPoints),
315
+ ),
316
+ // Files matching includeInBuild and other preset patterns
317
+ ...itermap(
318
+ (entry) =>
319
+ path.relative(workingDir, path.join(entry.parentPath, entry.name)),
320
+ walkDirWithPatterns(workingDir, appConfig?.includeInBuild),
321
+ ),
322
+ ]),
323
+ ).sort();
324
+
325
+ const workspaceRoot = (await findWorkspaceRoot(workingDir)) || workingDir;
326
+
327
+ if (workspaceRoot !== workingDir) {
328
+ const appDirRelPath = path.relative(workspaceRoot, workingDir);
329
+ const linkNames = ['zapierwrapper.js', 'index.js'];
330
+ for (const name of linkNames) {
331
+ if (fs.existsSync(path.join(workingDir, name))) {
332
+ zip.symlink(name, path.join(appDirRelPath, name), 0o644);
333
+ }
334
+ }
187
335
 
188
- const [dumbPaths, smartPaths, appConfig] = await Promise.all([
189
- listFiles(dir),
190
- requiredFiles({ cwd: dir, entryPoints }),
191
- getLinkedAppConfig(dir).catch(() => ({})),
192
- ]);
336
+ const filenames = ['package.json', 'definition.json'];
337
+ for (const name of filenames) {
338
+ const absPath = path.resolve(workingDir, name);
339
+ zip.file(absPath, { name, mode: 0o644 });
340
+ }
341
+ }
342
+
343
+ // Write required files to the zip
344
+ for (const relPath of relPaths) {
345
+ const absPath = path.resolve(workingDir, relPath);
346
+ const nameInZip = path.relative(workspaceRoot, absPath);
347
+ if (nameInZip === 'package.json' && workspaceRoot !== workingDir) {
348
+ // Ignore workspace root's package.json
349
+ continue;
350
+ }
351
+ zip.file(absPath, { name: nameInZip, mode: 0o644 });
352
+ }
353
+
354
+ // Next, find all symlinks that are either: (1) immediate children of any
355
+ // node_modules directory, or (2) located one directory level below a
356
+ // node_modules directory. (1) is for the case of node_modules/package_name.
357
+ // (2) is for the case of node_modules/@scope/package_name.
358
+ const nodeModulesDirs = new Set();
359
+ for (const relPath of relPaths) {
360
+ const nmDir = getNearestNodeModulesDir(workingDir, relPath);
361
+ if (nmDir) {
362
+ nodeModulesDirs.add(nmDir);
363
+ }
364
+ }
365
+
366
+ for (const relNmDir of nodeModulesDirs) {
367
+ const absNmDir = path.resolve(workingDir, relNmDir);
368
+ const symlinks = iterfilter(
369
+ (entry) => {
370
+ // Only include symlinks that are not in node_modules/.bin directories
371
+ return (
372
+ entry.isSymbolicLink() &&
373
+ !entry.parentPath.endsWith(`${path.sep}node_modules${path.sep}.bin`)
374
+ );
375
+ },
376
+ walkDirLimitedLevels(absNmDir, 2),
377
+ );
378
+ for (const symlink of symlinks) {
379
+ const absPath = path.resolve(
380
+ workingDir,
381
+ symlink.parentPath,
382
+ symlink.name,
383
+ );
384
+ const nameInZip = path.relative(workspaceRoot, absPath);
385
+ const targetInZip = path.relative(
386
+ symlink.parentPath,
387
+ fs.realpathSync(absPath),
388
+ );
389
+ zip.symlink(nameInZip, targetInZip, 0o644);
390
+ }
391
+ }
392
+ };
393
+
394
+ // Creates the build.zip file.
395
+ const makeBuildZip = async (
396
+ workingDir,
397
+ zipPath,
398
+ disableDependencyDetection,
399
+ ) => {
400
+ const zip = openZip(zipPath);
193
401
 
194
402
  if (disableDependencyDetection) {
195
- paths = dumbPaths;
403
+ // Ideally, if dependency detection works really well, we don't need to
404
+ // support --disable-dependency-detection at all. We might want to phase out
405
+ // this code path over time. Also, this doesn't handle workspaces.
406
+ await writeBuildZipDumbly(workingDir, zip);
196
407
  } else {
197
- let finalPaths = smartPaths.concat(
198
- dumbPaths.filter(forceIncludeDumbPath.bind(null, appConfig)),
199
- );
200
- finalPaths = _.uniq(finalPaths);
201
- finalPaths.sort();
202
- debug('\nZip files:');
203
- finalPaths.forEach((filePath) => debug(` ${filePath}`));
204
- debug('');
205
- paths = finalPaths;
408
+ await writeBuildZipSmartly(workingDir, zip);
206
409
  }
207
410
 
208
- await writeZipFromPaths(dir, zipPath, paths);
411
+ await zip.finish();
209
412
  };
210
413
 
211
- const makeSourceZip = async (dir, zipPath) => {
212
- const paths = await listFiles(dir);
213
- const finalPaths = respectGitIgnore(dir, paths);
214
- finalPaths.sort();
215
- debug('\nSource Zip files:');
216
- finalPaths.forEach((filePath) => debug(` ${filePath}`));
414
+ const makeSourceZip = async (workingDir, zipPath) => {
415
+ const relPaths = Array.from(
416
+ itermap(
417
+ (entry) =>
418
+ path.relative(workingDir, path.join(entry.parentPath, entry.name)),
419
+ walkDirWithPresetBlocklist(workingDir),
420
+ ),
421
+ );
422
+ const finalRelPaths = respectGitIgnore(workingDir, relPaths).sort();
423
+
424
+ const zip = openZip(zipPath);
425
+
426
+ debug('\nSource files:');
427
+ for (const relPath of finalRelPaths) {
428
+ if (relPath === 'definition.json' || relPath === 'zapierwrapper.js') {
429
+ // These two files are generated at build time;
430
+ // they're not part of the source code.
431
+ continue;
432
+ }
433
+
434
+ const absPath = path.resolve(workingDir, relPath);
435
+ debug(` ${absPath}`);
436
+
437
+ zip.file(absPath, { name: relPath, mode: 0o644 });
438
+ }
217
439
  debug();
218
- await writeZipFromPaths(dir, zipPath, finalPaths);
440
+
441
+ await zip.finish();
219
442
  };
220
443
 
221
444
  const maybeNotifyAboutOutdated = () => {
@@ -223,19 +446,19 @@ const maybeNotifyAboutOutdated = () => {
223
446
  // `build` won't run if package.json isn't there, so if we get to here we're good
224
447
  const requiredVersion = _.get(
225
448
  require(path.resolve('./package.json')),
226
- `dependencies.${constants.PLATFORM_PACKAGE}`,
449
+ `dependencies.${PLATFORM_PACKAGE}`,
227
450
  );
228
451
 
229
452
  if (requiredVersion) {
230
453
  const notifier = updateNotifier({
231
- pkg: { name: constants.PLATFORM_PACKAGE, version: requiredVersion },
232
- updateCheckInterval: constants.UPDATE_NOTIFICATION_INTERVAL,
454
+ pkg: { name: PLATFORM_PACKAGE, version: requiredVersion },
455
+ updateCheckInterval: UPDATE_NOTIFICATION_INTERVAL,
233
456
  });
234
457
 
235
458
  if (notifier.update && notifier.update.latest !== requiredVersion) {
236
459
  notifier.notify({
237
460
  message: `There's a newer version of ${colors.cyan(
238
- constants.PLATFORM_PACKAGE,
461
+ PLATFORM_PACKAGE,
239
462
  )} available.\nConsider updating the dependency in your\n${colors.cyan(
240
463
  'package.json',
241
464
  )} (${colors.grey(notifier.update.current)} → ${colors.green(
@@ -256,31 +479,82 @@ const maybeRunBuildScript = async (options = {}) => {
256
479
  );
257
480
 
258
481
  if (_.get(pJson, ['scripts', ZAPIER_BUILD_KEY])) {
259
- startSpinner(`Running ${ZAPIER_BUILD_KEY} script`);
482
+ if (options.printProgress) {
483
+ startSpinner(`Running ${ZAPIER_BUILD_KEY} script`);
484
+ }
485
+
260
486
  await runCommand('npm', ['run', ZAPIER_BUILD_KEY], options);
261
- endSpinner();
487
+
488
+ if (options.printProgress) {
489
+ endSpinner();
490
+ }
262
491
  }
263
492
  }
264
493
  };
265
494
 
266
- // Get `workspaces` from root package.json and convert them to absolute paths.
267
- // Returns an empty array if package.json can't be found.
268
- const listWorkspaces = (workspaceRoot) => {
269
- const packageJsonPath = path.join(workspaceRoot, 'package.json');
270
- if (!fs.existsSync(packageJsonPath)) {
271
- return [];
495
+ const extractMissingModulePath = (testDir, error) => {
496
+ // Extract relative path to print a more user-friendly error message
497
+ if (error.message && error.message.includes('MODULE_NOT_FOUND')) {
498
+ const searchString = `Cannot find module '${testDir}/`;
499
+ const idx = error.message.indexOf(searchString);
500
+ if (idx >= 0) {
501
+ const pathStart = idx + searchString.length;
502
+ const pathEnd = error.message.indexOf("'", pathStart);
503
+ if (pathEnd >= 0) {
504
+ const relPath = error.message.substring(pathStart, pathEnd);
505
+ return relPath;
506
+ }
507
+ }
272
508
  }
509
+ return null;
510
+ };
511
+
512
+ const testBuildZip = async (zipPath) => {
513
+ const osTmpDir = await fse.realpath(os.tmpdir());
514
+ const testDir = path.join(
515
+ osTmpDir,
516
+ 'zapier-' + crypto.randomBytes(4).toString('hex'),
517
+ );
273
518
 
274
- let packageJson;
275
519
  try {
276
- packageJson = require(packageJsonPath);
277
- } catch (err) {
278
- return [];
279
- }
520
+ await fse.ensureDir(testDir);
521
+ await decompress(zipPath, testDir);
280
522
 
281
- return (packageJson.workspaces || []).map((relpath) =>
282
- path.resolve(workspaceRoot, relpath),
283
- );
523
+ const wrapperPath = path.join(testDir, 'zapierwrapper.js');
524
+ if (!fs.existsSync(wrapperPath)) {
525
+ throw new Error('zapierwrapper.js not found in build.zip.');
526
+ }
527
+
528
+ const indexPath = path.join(testDir, 'index.js');
529
+ const indexExists = fs.existsSync(indexPath);
530
+
531
+ try {
532
+ await runCommand(process.execPath, ['zapierwrapper.js'], {
533
+ cwd: testDir,
534
+ timeout: 5000,
535
+ });
536
+ if (indexExists) {
537
+ await runCommand(process.execPath, ['index.js'], {
538
+ cwd: testDir,
539
+ timeout: 5000,
540
+ });
541
+ }
542
+ } catch (error) {
543
+ // Extract relative path to print a more user-friendly error message
544
+ const relPath = extractMissingModulePath(testDir, error);
545
+ if (relPath) {
546
+ throw new Error(
547
+ `Detected a missing file in build.zip: '${relPath}'\n` +
548
+ `You may have to add it to ${colors.bold.underline('includeInBuild')} ` +
549
+ `in your ${colors.bold.underline('.zapierapprc')} file.`,
550
+ );
551
+ }
552
+ throw error;
553
+ }
554
+ } finally {
555
+ // Clean up test directory
556
+ await fse.remove(testDir);
557
+ }
284
558
  };
285
559
 
286
560
  const _buildFunc = async ({
@@ -290,129 +564,83 @@ const _buildFunc = async ({
290
564
  printProgress = true,
291
565
  checkOutdated = true,
292
566
  } = {}) => {
293
- const zipPath = constants.BUILD_PATH;
294
- const sourceZipPath = constants.SOURCE_PATH;
295
- const wdir = process.cwd();
296
-
297
- const osTmpDir = await fse.realpath(os.tmpdir());
298
- const tmpDir = path.join(
299
- osTmpDir,
300
- 'zapier-' + crypto.randomBytes(4).toString('hex'),
301
- );
302
- debug('Using temp directory: ', tmpDir);
567
+ const maybeStartSpinner = printProgress ? startSpinner : () => {};
568
+ const maybeEndSpinner = printProgress ? endSpinner : () => {};
303
569
 
304
570
  if (checkOutdated) {
305
571
  maybeNotifyAboutOutdated();
306
572
  }
573
+ const appDir = process.cwd();
307
574
 
308
- await maybeRunBuildScript();
309
-
310
- // make sure our directories are there
311
- await ensureDir(tmpDir);
312
- await ensureDir(constants.BUILD_DIR);
313
-
314
- if (printProgress) {
315
- startSpinner('Copying project to temp directory');
575
+ let workingDir;
576
+ if (skipNpmInstall) {
577
+ workingDir = appDir;
578
+ debug('Building in app directory: ', workingDir);
579
+ } else {
580
+ const osTmpDir = await fse.realpath(os.tmpdir());
581
+ workingDir = path.join(
582
+ osTmpDir,
583
+ 'zapier-' + crypto.randomBytes(4).toString('hex'),
584
+ );
585
+ debug('Building in temp directory: ', workingDir);
316
586
  }
317
587
 
318
- const copyFilter = skipNpmInstall
319
- ? (src) => !src.endsWith('.zip')
320
- : undefined;
321
-
322
- await copyDir(wdir, tmpDir, { filter: copyFilter });
588
+ await maybeRunBuildScript({ printProgress });
323
589
 
324
- if (skipNpmInstall) {
325
- const corePackageDir = findCorePackageDir();
326
- const nodeModulesDir = path.dirname(corePackageDir);
327
- const workspaceRoot = path.dirname(nodeModulesDir);
328
- if (wdir !== workspaceRoot) {
329
- // If we're in here, it means the user is using npm/yarn workspaces
330
- const workspaces = listWorkspaces(workspaceRoot);
331
-
332
- await copyDir(nodeModulesDir, path.join(tmpDir, 'node_modules'), {
333
- filter: (src) => {
334
- if (src.endsWith('.zip')) {
335
- return false;
336
- }
337
- const stat = fse.lstatSync(src);
338
- if (stat.isSymbolicLink()) {
339
- const realPath = path.resolve(
340
- path.dirname(src),
341
- fse.readlinkSync(src),
342
- );
343
- for (const workspace of workspaces) {
344
- // Use minimatch to do glob pattern match. If match, it means the
345
- // symlink points to a workspace package, so we don't copy it.
346
- if (minimatch(realPath, workspace)) {
347
- return false;
348
- }
349
- }
350
- }
351
- return true;
352
- },
353
- onDirExists: (dir) => {
354
- // Don't overwrite existing sub-directories in node_modules
355
- return false;
356
- },
357
- });
358
- }
359
- }
590
+ // make sure our directories are there
591
+ await fse.ensureDir(workingDir);
592
+ const buildDir = path.join(appDir, BUILD_DIR);
593
+ await fse.ensureDir(buildDir);
360
594
 
361
- let output = {};
362
595
  if (!skipNpmInstall) {
363
- if (printProgress) {
364
- endSpinner();
365
- startSpinner('Installing project dependencies');
366
- }
367
- output = await runCommand('npm', ['install', '--production'], {
368
- cwd: tmpDir,
596
+ maybeStartSpinner('Copying project to temp directory');
597
+ const copyFilter = (src) => !src.endsWith('.zip');
598
+ await copyDir(appDir, workingDir, { filter: copyFilter });
599
+
600
+ maybeEndSpinner();
601
+ maybeStartSpinner('Installing project dependencies');
602
+ const output = await runCommand('npm', ['install', '--production'], {
603
+ cwd: workingDir,
369
604
  });
370
- }
371
605
 
372
- // `npm install` may fail silently without returning a non-zero exit code, need to check further here
373
- const corePath = path.join(
374
- tmpDir,
375
- 'node_modules',
376
- constants.PLATFORM_PACKAGE,
377
- );
378
- if (!fs.existsSync(corePath)) {
379
- throw new Error(
380
- 'Could not install dependencies properly. Error log:\n' + output.stderr,
381
- );
606
+ // `npm install` may fail silently without returning a non-zero exit code,
607
+ // need to check further here
608
+ const corePath = path.join(workingDir, 'node_modules', PLATFORM_PACKAGE);
609
+ if (!fs.existsSync(corePath)) {
610
+ throw new Error(
611
+ 'Could not install dependencies properly. Error log:\n' + output.stderr,
612
+ );
613
+ }
382
614
  }
383
615
 
384
- if (printProgress) {
385
- endSpinner();
386
- startSpinner('Applying entry point files');
387
- }
616
+ maybeEndSpinner();
617
+ maybeStartSpinner('Applying entry point files');
388
618
 
389
- await copyZapierWrapper(corePath, tmpDir);
619
+ const corePath = findCorePackageDir(workingDir);
620
+ await copyZapierWrapper(corePath, workingDir);
390
621
 
391
- if (printProgress) {
392
- endSpinner();
393
- startSpinner('Building app definition.json');
394
- }
622
+ maybeEndSpinner();
623
+ maybeStartSpinner('Building app definition.json');
395
624
 
396
625
  const rawDefinition = await localAppCommand(
397
626
  { command: 'definition' },
398
- tmpDir,
627
+ workingDir,
399
628
  false,
400
629
  );
401
- const fileWriteError = await writeFile(
402
- path.join(tmpDir, 'definition.json'),
403
- prettyJSONstringify(rawDefinition),
404
- );
405
630
 
406
- if (fileWriteError) {
407
- debug('\nFile Write Error:\n', fileWriteError, '\n');
631
+ try {
632
+ fs.writeFileSync(
633
+ path.join(workingDir, 'definition.json'),
634
+ prettyJSONstringify(rawDefinition),
635
+ );
636
+ } catch (err) {
637
+ debug('\nFile Write Error:\n', err, '\n');
408
638
  throw new Error(
409
- `Unable to write ${tmpDir}/definition.json, please check file permissions!`,
639
+ `Unable to write ${workingDir}/definition.json, please check file permissions!`,
410
640
  );
411
641
  }
412
642
 
413
- if (printProgress) {
414
- endSpinner();
415
- }
643
+ maybeEndSpinner();
416
644
 
417
645
  if (!skipValidation) {
418
646
  /**
@@ -421,12 +649,10 @@ const _buildFunc = async ({
421
649
  * (Remote - `validateApp`) Both the Schema, AppVersion, and Auths are validated
422
650
  */
423
651
 
424
- if (printProgress) {
425
- startSpinner('Validating project schema and style');
426
- }
652
+ maybeStartSpinner('Validating project schema and style');
427
653
  const validationErrors = await localAppCommand(
428
654
  { command: 'validate' },
429
- tmpDir,
655
+ workingDir,
430
656
  false,
431
657
  );
432
658
  if (validationErrors.length) {
@@ -450,9 +676,8 @@ const _buildFunc = async ({
450
676
  'We hit some style validation errors, try running `zapier validate` to see them!',
451
677
  );
452
678
  }
453
- if (printProgress) {
454
- endSpinner();
455
- }
679
+
680
+ maybeEndSpinner();
456
681
 
457
682
  if (_.get(styleChecksResponse, ['warnings', 'total_failures'])) {
458
683
  console.log(colors.yellow('WARNINGS:'));
@@ -468,43 +693,35 @@ const _buildFunc = async ({
468
693
  debug('\nWarning: Skipping Validation');
469
694
  }
470
695
 
471
- if (printProgress) {
472
- startSpinner('Zipping project and dependencies');
473
- }
474
- await makeZip(tmpDir, path.join(wdir, zipPath), disableDependencyDetection);
696
+ maybeStartSpinner('Zipping project and dependencies');
697
+
698
+ const zipPath = path.join(appDir, BUILD_PATH);
699
+ await makeBuildZip(workingDir, zipPath, disableDependencyDetection);
475
700
  await makeSourceZip(
476
- tmpDir,
477
- path.join(wdir, sourceZipPath),
701
+ workingDir,
702
+ path.join(appDir, SOURCE_PATH),
478
703
  disableDependencyDetection,
479
704
  );
480
705
 
481
- if (printProgress) {
482
- endSpinner();
483
- startSpinner('Testing build');
484
- }
485
-
486
- if (!isWindows()) {
487
- // TODO err, what should we do on windows?
488
-
489
- // tries to do a reproducible build at least
490
- // https://content.pivotal.io/blog/barriers-to-deterministic-reproducible-zip-files
491
- // https://reproducible-builds.org/tools/ or strip-nondeterminism
492
- await runCommand(
493
- 'find',
494
- ['.', '-exec', 'touch', '-t', '201601010000', '{}', '+'],
495
- { cwd: tmpDir },
496
- );
497
- }
706
+ maybeEndSpinner();
498
707
 
499
- if (printProgress) {
500
- endSpinner();
501
- startSpinner('Cleaning up temp directory');
708
+ if (skipNpmInstall) {
709
+ maybeStartSpinner('Cleaning up temp files');
710
+ await deleteZapierWrapper(workingDir);
711
+ fs.rmSync(path.join(workingDir, 'definition.json'));
712
+ maybeEndSpinner();
713
+ } else {
714
+ maybeStartSpinner('Cleaning up temp directory');
715
+ await fse.remove(workingDir);
716
+ maybeEndSpinner();
502
717
  }
503
718
 
504
- await removeDir(tmpDir);
505
-
506
- if (printProgress) {
507
- endSpinner();
719
+ if (!isWindows()) {
720
+ // "Testing build" doesn't work on Windows because of some permission issue
721
+ // with symlinks
722
+ maybeStartSpinner('Testing build');
723
+ await testBuildZip(zipPath);
724
+ maybeEndSpinner();
508
725
  }
509
726
 
510
727
  return zipPath;
@@ -535,9 +752,9 @@ const buildAndOrUpload = async (
535
752
 
536
753
  module.exports = {
537
754
  buildAndOrUpload,
538
- makeZip,
755
+ findRequiredFiles,
756
+ makeBuildZip,
539
757
  makeSourceZip,
540
- listFiles,
541
- requiredFiles,
542
758
  maybeRunBuildScript,
759
+ walkDirWithPresetBlocklist,
543
760
  };