zapier-platform-cli 17.2.0 → 17.3.1

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