apify-cli 0.19.2-beta.0 → 0.19.2-beta.10

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/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "apify-cli",
3
- "version": "0.19.2-beta.0",
3
+ "version": "0.19.2-beta.10",
4
4
  "description": "Apify command-line interface helps you create, develop, build and run Apify actors, and manage the Apify cloud platform.",
5
5
  "main": "index.js",
6
6
  "scripts": {
7
- "test": "cross-env APIFY_CLI_SKIP_UPDATE_CHECK=1 mocha --timeout 180000 --recursive",
8
- "test-python": "npm run test -- --grep '\\[python\\]'",
7
+ "test": "cross-env APIFY_CLI_SKIP_UPDATE_CHECK=1 vitest run",
8
+ "test-python": "cross-env APIFY_CLI_SKIP_UPDATE_CHECK=1 vitest run -t '.*\\[python\\]'",
9
9
  "lint": "eslint src test",
10
10
  "lint:fix": "eslint src test --fix",
11
11
  "commands-md": "npm run manifest && oclif-dev readme",
@@ -66,9 +66,9 @@
66
66
  "@root/walk": "^1.1.0",
67
67
  "adm-zip": "^0.5.10",
68
68
  "ajv": "^8.12.0",
69
- "apify-client": "^2.8.2",
69
+ "apify-client": "^2.9.0",
70
70
  "archiver-promise": "^1.0.0",
71
- "axios": "^1.6.1",
71
+ "axios": "^1.6.7",
72
72
  "chalk": "^4.1.2",
73
73
  "computer-name": "^0.1.0",
74
74
  "configparser": "^0.3.10",
@@ -98,12 +98,9 @@
98
98
  "@apify/eslint-config": "^0.4.0",
99
99
  "@oclif/dev-cli": "^1.26.0",
100
100
  "@oclif/test": "^2.1.0",
101
- "chai": "^4.3.4",
102
- "chai-match": "^1.1.1",
103
101
  "cross-env": "^7.0.3",
104
102
  "eslint": "^8.53.0",
105
- "mocha": "^10.0.0",
106
- "sinon": "^17.0.0"
103
+ "vitest": "^1.0.4"
107
104
  },
108
105
  "oclif": {
109
106
  "bin": "apify",
@@ -70,7 +70,7 @@ class CreateCommand extends ApifyCommand {
70
70
  try {
71
71
  fs.mkdirSync(actFolderDir);
72
72
  } catch (err) {
73
- if (err.code && err.code === 'EEXIST') {
73
+ if (err?.code === 'EEXIST') {
74
74
  outputs.error(`Cannot create new actor, directory '${actorName}' already exists. `
75
75
  + 'You can use "apify init" to create a local actor environment inside an existing directory.');
76
76
  return;
@@ -162,6 +162,7 @@ class CreateCommand extends ApifyCommand {
162
162
 
163
163
  if (dependenciesInstalled) {
164
164
  outputs.success(`Actor '${actorName}' was created. To run it, run "cd ${actorName}" and "apify run".`);
165
+ outputs.success('To run your code in the cloud, run "apify push" and deploy your code to Apify Console.');
165
166
  if (messages?.postCreate) {
166
167
  outputs.info(messages?.postCreate);
167
168
  }
@@ -9,7 +9,7 @@ const { createPrefilledInputFileFromInputSchema } = require('../lib/input_schema
9
9
  const outputs = require('../lib/outputs');
10
10
  const { ProjectAnalyzer } = require('../lib/project_analyzer');
11
11
  const { wrapScrapyProject } = require('../lib/scrapy-wrapper');
12
- const { setLocalConfig, setLocalEnv, getLocalConfig, getLocalConfigOrThrow, detectLocalActorLanguage } = require('../lib/utils');
12
+ const { setLocalConfig, setLocalEnv, getLocalConfig, getLocalConfigOrThrow, detectLocalActorLanguage, validateActorName } = require('../lib/utils');
13
13
 
14
14
  class InitCommand extends ApifyCommand {
15
15
  async run() {
@@ -34,8 +34,19 @@ class InitCommand extends ApifyCommand {
34
34
  outputs.warning(`Skipping creation of "${LOCAL_CONFIG_PATH}", the file already exists in the current directory.`);
35
35
  } else {
36
36
  if (!actorName) {
37
- const answer = await inquirer.prompt([{ name: 'actName', message: 'Actor name:', default: path.basename(cwd) }]);
38
- ({ actName: actorName } = answer);
37
+ let response = null;
38
+
39
+ while (!response) {
40
+ try {
41
+ const answer = await inquirer.prompt([{ name: 'actName', message: 'Actor name:', default: path.basename(cwd) }]);
42
+ validateActorName(answer.actName);
43
+ response = answer;
44
+ } catch (err) {
45
+ outputs.error(err.message);
46
+ }
47
+ }
48
+
49
+ ({ actName: actorName } = response);
39
50
  }
40
51
  // Migrate apify.json to .actor/actor.json
41
52
  const localConfig = { ...EMPTY_LOCAL_CONFIG, ...await getLocalConfigOrThrow() };
@@ -16,7 +16,7 @@ const { replaceSecretsValue } = require('../lib/secrets');
16
16
  const {
17
17
  getLocalUserInfo, purgeDefaultQueue, purgeDefaultKeyValueStore,
18
18
  purgeDefaultDataset, getLocalConfigOrThrow, getNpmCmd, checkIfStorageIsEmpty,
19
- detectLocalActorLanguage, isPythonVersionSupported, getPythonCommand, isNodeVersionSupported,
19
+ detectLocalActorLanguage, isPythonVersionSupported, getPythonCommand, isNodeVersionSupported, getLocalStorageDir,
20
20
  } = require('../lib/utils');
21
21
 
22
22
  class RunCommand extends ApifyCommand {
@@ -29,9 +29,12 @@ class RunCommand extends ApifyCommand {
29
29
  const packageJsonPath = path.join(cwd, 'package.json');
30
30
  const mainPyPath = path.join(cwd, 'src/__main__.py');
31
31
 
32
+ const projectType = ProjectAnalyzer.getProjectType(cwd);
33
+ const actualStoragePath = getLocalStorageDir();
34
+
32
35
  const packageJsonExists = fs.existsSync(packageJsonPath);
33
36
  const mainPyExists = fs.existsSync(mainPyPath);
34
- const isScrapyProject = ProjectAnalyzer.getProjectType(cwd) === PROJECT_TYPES.SCRAPY;
37
+ const isScrapyProject = projectType === PROJECT_TYPES.SCRAPY;
35
38
 
36
39
  if (!packageJsonExists && !mainPyExists && !isScrapyProject) {
37
40
  throw new Error(
@@ -40,25 +43,43 @@ class RunCommand extends ApifyCommand {
40
43
  );
41
44
  }
42
45
 
43
- if (fs.existsSync(LEGACY_LOCAL_STORAGE_DIR) && !fs.existsSync(DEFAULT_LOCAL_STORAGE_DIR)) {
44
- fs.renameSync(LEGACY_LOCAL_STORAGE_DIR, DEFAULT_LOCAL_STORAGE_DIR);
45
- warning("The legacy 'apify_storage' directory was renamed to 'storage' to align it with Apify SDK v3."
46
+ if (fs.existsSync(LEGACY_LOCAL_STORAGE_DIR) && !fs.existsSync(actualStoragePath)) {
47
+ fs.renameSync(LEGACY_LOCAL_STORAGE_DIR, actualStoragePath);
48
+ warning(`The legacy 'apify_storage' directory was renamed to '${actualStoragePath}' to align it with Apify SDK v3.`
46
49
  + ' Contents were left intact.');
47
50
  }
48
51
 
52
+ let CRAWLEE_PURGE_ON_START = '0';
53
+
49
54
  // Purge stores
50
55
  if (flags.purge) {
51
- await Promise.all([purgeDefaultQueue(), purgeDefaultKeyValueStore(), purgeDefaultDataset()]);
52
- info('All default local stores were purged.');
56
+ switch (projectType) {
57
+ case PROJECT_TYPES.CRAWLEE: {
58
+ CRAWLEE_PURGE_ON_START = '1';
59
+ break;
60
+ }
61
+ case PROJECT_TYPES.PRE_CRAWLEE_APIFY_SDK: {
62
+ await Promise.all([purgeDefaultQueue(), purgeDefaultKeyValueStore(), purgeDefaultDataset()]);
63
+ info('All default local stores were purged.');
64
+ break;
65
+ }
66
+ default: {
67
+ // TODO: Python SDK too
68
+ }
69
+ }
53
70
  }
71
+
72
+ // TODO: deprecate these flags
54
73
  if (flags.purgeQueue) {
55
74
  await purgeDefaultQueue();
56
75
  info('Default local request queue was purged.');
57
76
  }
77
+
58
78
  if (flags.purgeDataset) {
59
79
  await purgeDefaultDataset();
60
80
  info('Default local dataset was purged.');
61
81
  }
82
+
62
83
  if (flags.purgeKeyValueStore) {
63
84
  await purgeDefaultKeyValueStore();
64
85
  info('Default local key-value store was purged.');
@@ -74,7 +95,9 @@ class RunCommand extends ApifyCommand {
74
95
 
75
96
  // Attach env vars from local config files
76
97
  const localEnvVars = {
77
- [APIFY_ENV_VARS.LOCAL_STORAGE_DIR]: DEFAULT_LOCAL_STORAGE_DIR,
98
+ [APIFY_ENV_VARS.LOCAL_STORAGE_DIR]: actualStoragePath,
99
+ CRAWLEE_STORAGE_DIR: actualStoragePath,
100
+ CRAWLEE_PURGE_ON_START,
78
101
  };
79
102
  if (proxy && proxy.password) localEnvVars[APIFY_ENV_VARS.PROXY_PASSWORD] = proxy.password;
80
103
  if (userId) localEnvVars[APIFY_ENV_VARS.USER_ID] = userId;
package/src/lib/consts.js CHANGED
@@ -25,6 +25,8 @@ exports.LANGUAGE = {
25
25
 
26
26
  exports.PROJECT_TYPES = {
27
27
  SCRAPY: 'scrapy',
28
+ CRAWLEE: 'crawlee',
29
+ PRE_CRAWLEE_APIFY_SDK: 'apify',
28
30
  UNKNOWN: 'unknown',
29
31
  };
30
32
 
@@ -1,4 +1,6 @@
1
1
  const { PROJECT_TYPES } = require('./consts');
2
+ const { CrawleeAnalyzer } = require('./projects/CrawleeAnalyzer');
3
+ const { OldApifySDKAnalyzer } = require('./projects/OldApifySDKAnalyzer');
2
4
  const { ScrapyProjectAnalyzer } = require('./scrapy-wrapper/ScrapyProjectAnalyzer');
3
5
 
4
6
  const analyzers = [
@@ -6,6 +8,14 @@ const analyzers = [
6
8
  type: PROJECT_TYPES.SCRAPY,
7
9
  analyzer: ScrapyProjectAnalyzer,
8
10
  },
11
+ {
12
+ type: PROJECT_TYPES.CRAWLEE,
13
+ analyzer: CrawleeAnalyzer,
14
+ },
15
+ {
16
+ type: PROJECT_TYPES.PRE_CRAWLEE_APIFY_SDK,
17
+ analyzer: OldApifySDKAnalyzer,
18
+ },
9
19
  ];
10
20
 
11
21
  class ProjectAnalyzer {
@@ -17,6 +27,7 @@ class ProjectAnalyzer {
17
27
 
18
28
  return a.analyzer.isApplicable(pathname);
19
29
  });
30
+
20
31
  return analyzer?.type || PROJECT_TYPES.UNKNOWN;
21
32
  }
22
33
  }
@@ -0,0 +1,37 @@
1
+ const { existsSync, readFileSync } = require('fs');
2
+ const { join } = require('path');
3
+
4
+ const CRAWLEE_PACKAGES = [
5
+ 'crawlee',
6
+ '@crawlee/core',
7
+ '@crawlee/puppeteer',
8
+ '@crawlee/playwright',
9
+ '@crawlee/cheerio',
10
+ '@crawlee/jsdom',
11
+ '@crawlee/linkedom',
12
+ '@crawlee/http',
13
+ '@crawlee/browser',
14
+ '@crawlee/basic',
15
+ ];
16
+
17
+ class CrawleeAnalyzer {
18
+ static isApplicable(pathname) {
19
+ const hasPackageJson = existsSync(join(pathname, 'package.json'));
20
+
21
+ if (!hasPackageJson) {
22
+ return false;
23
+ }
24
+
25
+ const packageJson = readFileSync(join(pathname, 'package.json'), 'utf8');
26
+
27
+ try {
28
+ const packageJsonParsed = JSON.parse(packageJson);
29
+
30
+ return CRAWLEE_PACKAGES.some((pkg) => packageJsonParsed?.dependencies?.[pkg] !== undefined);
31
+ } catch (err) {
32
+ return false;
33
+ }
34
+ }
35
+ }
36
+
37
+ exports.CrawleeAnalyzer = CrawleeAnalyzer;
@@ -0,0 +1,61 @@
1
+ const { existsSync, readFileSync } = require('fs');
2
+ const { join } = require('path');
3
+
4
+ const { lt } = require('semver');
5
+
6
+ const VERSION_WHEN_APIFY_MOVED_TO_CRAWLEE = '3.0.0';
7
+ const CRAWLEE_PACKAGES = [
8
+ 'crawlee',
9
+ '@crawlee/core',
10
+ '@crawlee/puppeteer',
11
+ '@crawlee/playwright',
12
+ '@crawlee/cheerio',
13
+ '@crawlee/jsdom',
14
+ '@crawlee/linkedom',
15
+ '@crawlee/http',
16
+ '@crawlee/browser',
17
+ '@crawlee/basic',
18
+ ];
19
+
20
+ class OldApifySDKAnalyzer {
21
+ static isApplicable(pathname) {
22
+ const hasPackageJson = existsSync(join(pathname, 'package.json'));
23
+
24
+ if (!hasPackageJson) {
25
+ return false;
26
+ }
27
+
28
+ const packageJson = readFileSync(join(pathname, 'package.json'), 'utf8');
29
+
30
+ try {
31
+ const packageJsonParsed = JSON.parse(packageJson);
32
+
33
+ // If they have crawlee as a dependency, likely to use crawlee
34
+ if (CRAWLEE_PACKAGES.some((pkg) => packageJsonParsed?.dependencies?.[pkg] !== undefined)) {
35
+ return false;
36
+ }
37
+
38
+ const apifyVersion = packageJsonParsed?.dependencies?.apify;
39
+ if (!apifyVersion) {
40
+ return false;
41
+ }
42
+
43
+ // We cannot infer
44
+ if (apifyVersion === '*') {
45
+ return false;
46
+ }
47
+
48
+ let actualVersion = apifyVersion;
49
+
50
+ if (apifyVersion.startsWith('~') || apifyVersion.startsWith('^')) {
51
+ actualVersion = apifyVersion.slice(1);
52
+ }
53
+
54
+ return lt(actualVersion, VERSION_WHEN_APIFY_MOVED_TO_CRAWLEE);
55
+ } catch (err) {
56
+ return false;
57
+ }
58
+ }
59
+ }
60
+
61
+ exports.OldApifySDKAnalyzer = OldApifySDKAnalyzer;
@@ -9,7 +9,7 @@ const inquirer = require('inquirer');
9
9
 
10
10
  const { ScrapyProjectAnalyzer } = require('./ScrapyProjectAnalyzer');
11
11
  const outputs = require('../outputs');
12
- const { downloadAndUnzip } = require('../utils');
12
+ const { downloadAndUnzip, sanitizeActorName } = require('../utils');
13
13
 
14
14
  /**
15
15
  * Files that should be concatenated instead of copied (and overwritten).
@@ -86,7 +86,7 @@ async function wrapScrapyProject({ projectPath }) {
86
86
  }
87
87
 
88
88
  const templateBindings = {
89
- botName: analyzer.settings.BOT_NAME,
89
+ botName: sanitizeActorName(analyzer.settings.BOT_NAME),
90
90
  scrapy_settings_module: analyzer.configuration.get('settings', 'default'),
91
91
  apify_module_path: `${analyzer.settings.BOT_NAME}.apify`,
92
92
  spider_class_name: analyzer.getAvailableSpiders()[spiderIndex].class_name,
package/src/lib/utils.js CHANGED
@@ -21,6 +21,7 @@ const {
21
21
  const AdmZip = require('adm-zip');
22
22
  const { ApifyClient } = require('apify-client');
23
23
  const archiver = require('archiver-promise');
24
+ const axios = require('axios');
24
25
  const escapeStringRegexp = require('escape-string-regexp');
25
26
  const globby = require('globby');
26
27
  const inquirer = require('inquirer');
@@ -80,7 +81,7 @@ const MIGRATED_APIFY_JSON_PROPERTIES = ['name', 'version', 'buildTag'];
80
81
  const getLocalStorageDir = () => {
81
82
  const envVar = APIFY_ENV_VARS.LOCAL_STORAGE_DIR;
82
83
 
83
- return process.env[envVar] || DEFAULT_LOCAL_STORAGE_DIR;
84
+ return process.env[envVar] || process.env.CRAWLEE_STORAGE_DIR || DEFAULT_LOCAL_STORAGE_DIR;
84
85
  };
85
86
  const getLocalKeyValueStorePath = (storeId) => {
86
87
  const envVar = ACTOR_ENV_VARS.DEFAULT_KEY_VALUE_STORE_ID;
@@ -139,7 +140,14 @@ const getApifyClientOptions = (token, apiBaseUrl) => {
139
140
  token,
140
141
  baseUrl: apiBaseUrl || process.env.APIFY_CLIENT_BASE_URL,
141
142
  requestInterceptors: [(config) => {
142
- config.headers = { ...APIFY_CLIENT_DEFAULT_HEADERS, ...config.headers };
143
+ if (!config.headers) {
144
+ config.headers = new axios.AxiosHeaders();
145
+ }
146
+
147
+ for (const [key, value] of Object.entries(APIFY_CLIENT_DEFAULT_HEADERS)) {
148
+ config.headers[key] = value;
149
+ }
150
+
143
151
  return config;
144
152
  }],
145
153
  };
@@ -536,6 +544,19 @@ const validateActorName = (actorName) => {
536
544
  }
537
545
  };
538
546
 
547
+ const sanitizeActorName = (actorName) => {
548
+ let sanitizedName = actorName
549
+ .replaceAll(/[^a-zA-Z0-9-]/g, '-');
550
+
551
+ if (sanitizedName.length < ACTOR_NAME.MIN_LENGTH) {
552
+ sanitizedName = `${sanitizedName}-apify-actor`;
553
+ }
554
+
555
+ sanitizedName = sanitizedName.replaceAll(/^-+/g, '').replaceAll(/-+$/g, '');
556
+
557
+ return sanitizedName.slice(0, ACTOR_NAME.MAX_LENGTH);
558
+ };
559
+
539
560
  const getPythonCommand = (directory) => {
540
561
  const pythonVenvPath = /^win/.test(process.platform)
541
562
  ? 'Scripts/python.exe'
@@ -665,4 +686,5 @@ module.exports = {
665
686
  detectNpmVersion,
666
687
  detectLocalActorLanguage,
667
688
  downloadAndUnzip,
689
+ sanitizeActorName,
668
690
  };