├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .npmignore ├── .npmrc ├── .nvmrc ├── .prettierrc.js ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── jest.config.ts ├── lib ├── configuration-schema.json └── sdk.js ├── package-lock.json ├── package.json ├── src ├── Logger.ts ├── SdkContext.ts ├── __fixtures__ │ ├── config.json │ ├── test.js │ ├── test.json │ ├── test.md │ ├── test2.js │ ├── test2.json │ ├── unconfigured.js │ └── wrongConfig.json ├── configuration.schema.ts ├── constants.ts ├── errors.ts ├── fileUpload.ts ├── files.ts ├── requests.ts ├── sdk.test.ts ├── sdk.ts └── types.ts ├── tsconfig.json └── tsconfig.prod.json /.eslintignore: -------------------------------------------------------------------------------- 1 | lib -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | /** @type {import('eslint').Linter.Config} */ 2 | /* eslint-disable @typescript-eslint/no-require-imports*/ 3 | module.exports = { 4 | env: { 5 | browser: true, 6 | es6: true, 7 | node: true, 8 | }, 9 | parser: "@typescript-eslint/parser", 10 | plugins: [ 11 | "deprecation", 12 | "@typescript-eslint", 13 | "prefer-arrow", 14 | "toplevel", 15 | "regexp", 16 | "promise", 17 | "prettier", 18 | "sonarjs", 19 | ], 20 | extends: [ 21 | "eslint:recommended", 22 | "plugin:@typescript-eslint/eslint-recommended", 23 | "plugin:@typescript-eslint/recommended", 24 | "plugin:regexp/recommended", 25 | "plugin:promise/recommended", 26 | "plugin:prettier/recommended", 27 | ], 28 | parserOptions: { 29 | project: ["tsconfig.json"], 30 | ecmaVersion: 9, 31 | sourceType: "module", 32 | }, 33 | rules: { 34 | "class-methods-use-this": "error", 35 | "toplevel/no-toplevel-var": "error", 36 | "toplevel/no-toplevel-let": "error", 37 | "promise/no-callback-in-promise": "error", 38 | "deprecation/deprecation": "warn", 39 | "@typescript-eslint/no-extra-semi": "off", 40 | "prettier/prettier": [ 41 | "error", 42 | {}, 43 | { 44 | usePrettierrc: true, 45 | }, 46 | ], 47 | "@typescript-eslint/no-unused-vars": ["error", { vars: "all", args: "after-used", ignoreRestSiblings: true }], 48 | "@typescript-eslint/no-var-requires": "off", // Disallows the use of require statements except in import statements 49 | "@typescript-eslint/no-explicit-any": ["error"], // Disallow the use of "any"", 50 | curly: "error", 51 | "@typescript-eslint/no-loss-of-precision": "warn", 52 | "no-loss-of-precision": "warn", 53 | "@typescript-eslint/no-floating-promises": "error", // Promises returned by functions must be handled appropriately 54 | "no-duplicate-imports": "error", 55 | "linebreak-style": ["error", "unix"], 56 | eqeqeq: ["error", "always"], 57 | "no-implied-eval": ["error"], 58 | "no-new-func": ["error"], 59 | "no-new-wrappers": ["error"], 60 | "no-void": ["error"], 61 | "wrap-iife": ["error", "any"], 62 | radix: ["error", "always"], 63 | yoda: ["error", "never"], 64 | "comma-dangle": ["error", "only-multiline"], 65 | "no-array-constructor": ["error"], 66 | "no-var": ["error"], 67 | "no-empty": ["error"], 68 | "no-debugger": ["error"], 69 | "no-unreachable": ["error"], 70 | "no-caller": ["error"], 71 | "no-with": ["error"], 72 | "eol-last": ["warn", "always"], 73 | "no-constant-condition": ["off"], 74 | "no-mixed-spaces-and-tabs": ["off"], 75 | "no-multi-spaces": ["warn"], 76 | "no-return-await": ["error"], 77 | "no-sequences": ["error"], 78 | "no-useless-call": ["error"], 79 | "no-useless-concat": ["error"], 80 | "no-undefined": ["off"], 81 | "no-undef-init": ["error"], 82 | "brace-style": [ 83 | "warn", 84 | "1tbs", 85 | { 86 | allowSingleLine: true, 87 | }, 88 | ], 89 | "block-spacing": ["warn"], 90 | "space-in-parens": ["warn"], 91 | "keyword-spacing": ["warn"], 92 | "space-infix-ops": ["warn"], 93 | "no-new-object": ["error"], 94 | "no-nested-ternary": ["error"], 95 | "no-multi-assign": ["warn"], 96 | "no-lonely-if": ["warn"], 97 | "new-parens": ["error"], 98 | "new-cap": ["error"], 99 | "require-await": "off", 100 | "@typescript-eslint/require-await": "error", 101 | "func-style": ["warn", "declaration"], 102 | "max-statements-per-line": [ 103 | "warn", 104 | { 105 | max: 2, 106 | }, 107 | ], 108 | "max-nested-callbacks": [ 109 | "warn", 110 | { 111 | max: 3, 112 | }, 113 | ], 114 | "prefer-const": "error", // Suggest using const when possible 115 | "@typescript-eslint/prefer-for-of": "error", // Recommends a ‘for-of’ loop over a standard ‘for’ loop if the index is only used to access the array being iterated 116 | // "@typescript-eslint/consistent-type-definitions": ["error"], // interface over type when applicable 117 | }, 118 | overrides: [ 119 | { 120 | settings: { 121 | jest: { 122 | version: require("jest/package.json").version, 123 | }, 124 | }, 125 | files: ["**__tests__**", "**.test.ts", "**.spec.ts", "**__fixtures__**"], 126 | plugins: ["jest", "jest-formatting"], 127 | extends: ["plugin:jest/recommended", "plugin:jest-formatting/recommended"], 128 | env: { 129 | "jest/globals": true, 130 | }, 131 | rules: { 132 | "max-nested-callbacks": 0, 133 | "sonarjs/no-duplicate-string": "off", 134 | "jest/expect-expect": [ 135 | "warn", 136 | { 137 | assertFunctionNames: ["expect*", "*.then*"], 138 | additionalTestBlockFunctions: [], 139 | }, 140 | ], 141 | }, 142 | }, 143 | ], 144 | } 145 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | *~ 3 | *.swo 4 | .DS_Store 5 | node_modules/ 6 | test/ 7 | build/* 8 | .vscodes/* 9 | coverage/ -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .git 2 | .gitignore 3 | *.swp 4 | *~ 5 | *.swo 6 | .DS_Store 7 | node_modules/ 8 | test/ 9 | src/ 10 | coverage/ 11 | lib/ 12 | .* 13 | jest.config.ts 14 | tsconfig.* -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict = true -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v20 2 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "semi": false, 3 | "trailingComma": "all", 4 | "singleQuote": false, 5 | "bracketSpacing": true, 6 | "tabWidth": 4, 7 | "useTabs": true, 8 | "printWidth": 120 9 | } 10 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | } 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright (c) 2017-2018, Phantombuster 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted, provided that the above 7 | copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 10 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 11 | AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 12 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 13 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE 14 | OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 15 | PERFORMANCE OF THIS SOFTWARE. 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | phantombuster-sdk 2 | ================= 3 | 4 | https://phantombuster.com/ 5 | 6 | Provides the `phantombuster` command to facilitate development of scripts for Phantombuster. 7 | 8 | To install: 9 | 10 | `npm install -g phantombuster-sdk` (recommended) 11 | 12 | or 13 | 14 | `npm install -D phantombuster-sdk` 15 | 16 | For now, the only feature provided is the uploading of scripts via Phantombuster's API. 17 | 18 | phantombuster.cson 19 | ------------------ 20 | 21 | The SDK works with `phantombuster.cson`. This file must be located alongside the scripts, in the same directory (or a parent directory). 22 | 23 | This file is simple and self explanatory. The small example below is enough to understand everything: 24 | 25 | [ 26 | name: 'Excellent project 1' # Arbitrary name, only used for logs 27 | apiKey: 'xxxx' # Phantombuster API key (which identifies the account) 28 | # Mappings of Phantombuster script names to local script files (relative to the phantombuster.cson file) 29 | scripts: 30 | 'scraping.js': 'project1/scraping.js' 31 | 'export.js': 'project1/export.js' 32 | , 33 | name: 'Cool project 2' 34 | apiKey: 'another xxxx' 35 | scripts: 36 | 'some-casperjs-browsing.js': 'folder/script.js' 37 | ] 38 | 39 | Usage 40 | ----- 41 | 42 | `phantombuster [-c config.cson] [script.coffee [other.coffee...]]` 43 | 44 | * The most typical usage is to watch for file modification while coding. Simply execute `phantombuster` in a directory containing `phantombuster.cson`. 45 | * Specify a different file than `phantombuster.cson`: `phantombuster -c config.cson` 46 | * Upload a specific script to Phantombuster (without watching): `phantombuster project/script.coffee` (must be a value in one of the `scripts` objects in `phantombuster.cson`) 47 | * Update your whole project: `phantombuster project/*.coffee` 48 | 49 | See the full documentation here: https://hub.phantombuster.com/docs/sdk 50 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | import type { JestConfigWithTsJest } from "ts-jest/dist/types" 2 | 3 | const config: JestConfigWithTsJest = { 4 | preset: "ts-jest", 5 | testEnvironment: "node", 6 | transform: { 7 | "^.+\\.ts$": "@swc/jest", 8 | }, 9 | modulePathIgnorePatterns: [".*__fixtures__.*.js"], 10 | collectCoverageFrom: [ 11 | "src/**/*.ts", 12 | "src/**/*.js", 13 | "!**/*.test.{js,ts,tsx}", 14 | "!**/*.spec.{js,ts,tsx}", 15 | "!**/*.d.ts", 16 | ], 17 | coverageThreshold: { 18 | global: { 19 | branches: 90, 20 | functions: 90, 21 | lines: 90, 22 | statements: 90, 23 | }, 24 | }, 25 | } 26 | export default config 27 | -------------------------------------------------------------------------------- /lib/configuration-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema#", 3 | "type": "array", 4 | "items": { 5 | "type": "object", 6 | "properties": { 7 | "name": { 8 | "type": "string", 9 | "minLength": 1 10 | }, 11 | "endpoint": { 12 | "type": "string", 13 | "format": "uri" 14 | }, 15 | "apiKey": { 16 | "type": "string", 17 | "pattern": "^[\\w-:]{5,50}$" 18 | }, 19 | "scripts": { 20 | "type": "object", 21 | "patternProperties": { 22 | "^[\\w\\. -]{1,50}\\.(?:coffee|js)$": { 23 | "type": "string", 24 | "minLength": 1 25 | } 26 | }, 27 | "additionalProperties": false 28 | } 29 | }, 30 | "required": [ 31 | "name", 32 | "apiKey" 33 | ], 34 | "additionalProperties": false 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /lib/sdk.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | // Generated by CoffeeScript 1.12.7 3 | var argv, baseDir, config, configPath, cson, datePrefix, defaultEndpoint, e, fs, i, len, loadConfig, needle, path, ref, ref1, script, updateScript, updateStoreInfo, validate, watch, watchOptions; 4 | 5 | fs = require('fs'); 6 | 7 | path = require('path'); 8 | 9 | cson = require('cson'); 10 | 11 | needle = require('needle'); 12 | 13 | watch = require('node-watch'); 14 | 15 | validate = require('is-my-json-valid')(require('./configuration-schema.json')); 16 | 17 | argv = require('yargs').argv; 18 | 19 | defaultEndpoint = 'https://phantombuster.com/api/v1'; 20 | 21 | configPath = argv.c || 'phantombuster.cson'; 22 | 23 | datePrefix = function() { 24 | return (new Date).toLocaleTimeString() + ' - '; 25 | }; 26 | 27 | loadConfig = function(configPath) { 28 | var account, config, envVar, i, len; 29 | config = cson.load(configPath); 30 | if (validate(config)) { 31 | for (i = 0, len = config.length; i < len; i++) { 32 | account = config[i]; 33 | if (account.apiKey.indexOf("ENV:") === 0) { 34 | envVar = account.apiKey.replace("ENV:", ""); 35 | account.apiKey = process.env[envVar]; 36 | if ((typeof account.apiKey !== "string") || (account.apiKey.length < 10) || (account.apiKey.length > 50)) { 37 | console.log(account.name + ": Environment variable \"" + envVar + "\" does not contain a valid API key"); 38 | process.exit(1); 39 | } 40 | } 41 | } 42 | return config; 43 | } else { 44 | console.log("" + (datePrefix()) + configPath + " is not a correct SDK configuration file"); 45 | console.log(JSON.stringify(validate.errors)); 46 | return process.exit(1); 47 | } 48 | }; 49 | 50 | try { 51 | configPath = fs.realpathSync(configPath); 52 | baseDir = path.dirname(configPath); 53 | config = loadConfig(configPath); 54 | updateStoreInfo = function(updatedPath, fileType) { 55 | var account, i, jsonFile, len, linkedScriptCoffee, linkedScriptJs, localScript, mdFile, pbScript, ref, ref1, upload; 56 | linkedScriptJs = updatedPath.replace(new RegExp(fileType + '$'), 'js'); 57 | linkedScriptCoffee = updatedPath.replace(new RegExp(fileType + '$'), 'coffee'); 58 | mdFile = updatedPath.replace(new RegExp(fileType + '$'), 'md'); 59 | jsonFile = updatedPath.replace(new RegExp(fileType + '$'), 'json'); 60 | upload = function(account, pbScript, localScript) { 61 | return fs.readFile(jsonFile, function(err, jsonText) { 62 | var e, jsonErr; 63 | if (err && (err.code !== 'ENOENT')) { 64 | return console.log("" + (datePrefix()) + account.name + ": [API store settings] " + jsonFile + ": " + (err.toString())); 65 | } else { 66 | if (err) { 67 | jsonText = ''; 68 | } else { 69 | try { 70 | JSON.parse(jsonText); 71 | } catch (error) { 72 | e = error; 73 | jsonErr = e; 74 | } 75 | } 76 | if (jsonErr) { 77 | return console.log("" + (datePrefix()) + account.name + ": [API store settings] " + jsonFile + ": " + (jsonErr.toString())); 78 | } else { 79 | return fs.readFile(mdFile, function(err, mdText) { 80 | var options, payload; 81 | if (err && (err.code !== 'ENOENT')) { 82 | return console.log("" + (datePrefix()) + account.name + ": [API store settings] " + mdFile + ": " + (err.toString())); 83 | } else { 84 | if (err) { 85 | mdText = ''; 86 | } 87 | options = { 88 | json: true, 89 | headers: { 90 | 'X-Phantombuster-Key-1': account.apiKey 91 | } 92 | }; 93 | payload = { 94 | infoString: jsonText.toString(), 95 | markdown: mdText.toString() 96 | }; 97 | return needle.post((account.endpoint || defaultEndpoint) + "/store-info/by-name/" + pbScript, payload, options, function(err, res) { 98 | var ref, ref1, ref2; 99 | if (err) { 100 | return console.log("" + (datePrefix()) + account.name + ": [API store settings] " + localScript + ": " + (err.toString())); 101 | } else { 102 | if (((ref = res.body) != null ? ref.status : void 0) === 'success') { 103 | return console.log("" + (datePrefix()) + account.name + ": [API store settings] " + localScript + " -> " + pbScript); 104 | } else { 105 | return console.log("" + (datePrefix()) + account.name + ": [API store settings] " + localScript + ": " + (((ref1 = res.body) != null ? ref1.status : void 0) != null ? res.body.status : "Error") + ": " + (((ref2 = res.body) != null ? ref2.message : void 0) != null ? res.body.message : "HTTP " + res.statusCode)); 106 | } 107 | } 108 | }); 109 | } 110 | }); 111 | } 112 | } 113 | }); 114 | }; 115 | for (i = 0, len = config.length; i < len; i++) { 116 | account = config[i]; 117 | ref = account.scripts; 118 | for (pbScript in ref) { 119 | localScript = ref[pbScript]; 120 | if ((ref1 = path.join(baseDir, localScript)) === linkedScriptJs || ref1 === linkedScriptCoffee) { 121 | upload(account, pbScript, localScript); 122 | return true; 123 | } 124 | } 125 | } 126 | return false; 127 | }; 128 | updateScript = function(updatedPath) { 129 | var account, fileExt, i, len, localScript, pbScript, ref, upload; 130 | fileExt = path.extname(updatedPath); 131 | if (fileExt === '.md' || fileExt === '.json') { 132 | return updateStoreInfo(updatedPath, fileExt.replace('.', '')); 133 | } else { 134 | upload = function(account, pbScript, localScript, updatedPath) { 135 | return fs.readFile(updatedPath, function(err, text) { 136 | var options, payload; 137 | if (err) { 138 | return console.log("" + (datePrefix()) + account.name + ": " + localScript + ": " + (err.toString())); 139 | } else { 140 | options = { 141 | json: true, 142 | headers: { 143 | 'X-Phantombuster-Key-1': account.apiKey 144 | } 145 | }; 146 | payload = { 147 | text: text.toString(), 148 | source: 'sdk' 149 | }; 150 | return needle.post((account.endpoint || defaultEndpoint) + "/script/" + pbScript, payload, options, function(err, res) { 151 | var ref, ref1, ref2; 152 | if (err) { 153 | return console.log("" + (datePrefix()) + account.name + ": " + localScript + ": " + (err.toString())); 154 | } else { 155 | if (((ref = res.body) != null ? ref.status : void 0) === 'success') { 156 | return console.log("" + (datePrefix()) + account.name + ": " + localScript + " -> " + pbScript + (typeof res.body.data === 'number' ? ' (new script created)' : '')); 157 | } else { 158 | return console.log("" + (datePrefix()) + account.name + ": " + localScript + ": " + (((ref1 = res.body) != null ? ref1.status : void 0) != null ? res.body.status : "Error") + ": " + (((ref2 = res.body) != null ? ref2.message : void 0) != null ? res.body.message : "HTTP " + res.statusCode)); 159 | } 160 | } 161 | }); 162 | } 163 | }); 164 | }; 165 | for (i = 0, len = config.length; i < len; i++) { 166 | account = config[i]; 167 | ref = account.scripts; 168 | for (pbScript in ref) { 169 | localScript = ref[pbScript]; 170 | if (path.join(baseDir, localScript) === updatedPath) { 171 | upload(account, pbScript, localScript, updatedPath); 172 | return true; 173 | } 174 | } 175 | } 176 | return false; 177 | } 178 | }; 179 | if ((ref = argv._) != null ? ref.length : void 0) { 180 | ref1 = argv._; 181 | for (i = 0, len = ref1.length; i < len; i++) { 182 | script = ref1[i]; 183 | if (!updateScript(fs.realpathSync(script))) { 184 | console.log("" + (datePrefix()) + script + ": Not found in configuration"); 185 | } 186 | } 187 | } else { 188 | watchOptions = { 189 | recursive: true, 190 | filter: (function(f) { 191 | return !/node_modules/.test(f); 192 | }) 193 | }; 194 | watch(baseDir, watchOptions, function(event, updatedPath) { 195 | if (event === "update") { 196 | if (updatedPath === configPath) { 197 | config = loadConfig(updatedPath); 198 | return console.log("" + (datePrefix()) + updatedPath + ": Configuration reloaded"); 199 | } else { 200 | return updateScript(updatedPath); 201 | } 202 | } 203 | }); 204 | } 205 | } catch (error) { 206 | e = error; 207 | console.log(e.toString()); 208 | process.exit(1); 209 | } 210 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "phantombuster-sdk", 3 | "version": "1.0.0", 4 | "description": "Phantombuster's SDK", 5 | "main": "build/sdk.js", 6 | "scripts": { 7 | "test": "jest", 8 | "prepublish": "npm run build", 9 | "prepublishOnly": "npm run build", 10 | "build": "npx tsc -p tsconfig.prod.json", 11 | "watch": "npm run build -- --watch" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/phantombuster/sdk.git" 16 | }, 17 | "engines": { 18 | "node": "~20", 19 | "npm": ">=10" 20 | }, 21 | "keywords": [ 22 | "phantombuster", 23 | "casperjs", 24 | "phantomjs", 25 | "sdk", 26 | "headless", 27 | "chrome", 28 | "scraping" 29 | ], 30 | "author": "Martin Tapia (https://phantombuster.com)", 31 | "license": "ISC", 32 | "bugs": { 33 | "url": "https://github.com/phantombuster/sdk/issues", 34 | "email": "martin@phantombuster.com" 35 | }, 36 | "homepage": "https://github.com/phantombuster/sdk", 37 | "preferGlobal": true, 38 | "bin": { 39 | "phantombuster": "build/sdk.js" 40 | }, 41 | "devDependencies": { 42 | "@swc/core": "^1.7.23", 43 | "@swc/jest": "^0.2.36", 44 | "@types/cson": "^7.20.3", 45 | "@types/eslint": "^8.56.12", 46 | "@types/jest": "^29.5.12", 47 | "@types/needle": "^3.3.0", 48 | "@types/node": "^22.5.2", 49 | "@typescript-eslint/eslint-plugin": "^8.4.0", 50 | "@typescript-eslint/parser": "^8.4.0", 51 | "as-typed": "^1.3.2", 52 | "eslint": "^8.57.0", 53 | "eslint-config-prettier": "^9.1.0", 54 | "eslint-plugin-deprecation": "^3.0.0", 55 | "eslint-plugin-jest": "^28.8.2", 56 | "eslint-plugin-jest-formatting": "^3.1.0", 57 | "eslint-plugin-prefer-arrow": "^1.2.3", 58 | "eslint-plugin-prettier": "^5.2.1", 59 | "eslint-plugin-promise": "^7.1.0", 60 | "eslint-plugin-regexp": "^2.6.0", 61 | "eslint-plugin-sonarjs": "^2.0.2", 62 | "eslint-plugin-toplevel": "^1.1.0", 63 | "jest": "^29.7.0", 64 | "prettier": "^3.3.3", 65 | "ts-jest": "^29.2.5", 66 | "ts-node": "^10.9.2", 67 | "typescript": "^5.5.4" 68 | }, 69 | "dependencies": { 70 | "cson": "3.0.x", 71 | "is-my-json-valid": "^2.20.6", 72 | "needle": "^3.3.1", 73 | "node-watch": "^0.7.4", 74 | "yargs": "^17.7.2" 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Logger.ts: -------------------------------------------------------------------------------- 1 | function datePrefix() { 2 | return new Date().toLocaleTimeString() + " - " 3 | } 4 | 5 | export const Logger = { 6 | log(...[firstParams, ...params]: unknown[]) { 7 | console.log(`${datePrefix()}${firstParams}`, ...params) 8 | }, 9 | error(err: unknown) { 10 | if (err instanceof Error) { 11 | Logger.log(err.message) 12 | } else { 13 | Logger.log(err) 14 | } 15 | }, 16 | } 17 | -------------------------------------------------------------------------------- /src/SdkContext.ts: -------------------------------------------------------------------------------- 1 | import path from "path" 2 | 3 | import cson from "cson" 4 | import { realpathSync } from "node:fs" 5 | import validate from "is-my-json-valid" 6 | 7 | import { InvalidEnvVarError, WrongConfigFileError } from "./errors" 8 | import conf from "./configuration.schema" 9 | import type { ConfType, IScriptConfig } from "./types" 10 | import { extentionRegexp } from "./constants" 11 | import { uploadScriptInfo, uploadScript } from "./fileUpload" 12 | 13 | const isConfValid = validate(conf as unknown as Parameters[0]) as ((a: unknown) => a is ConfType) & 14 | ReturnType 15 | 16 | function loadConfig(configPath: string) { 17 | const config = cson.load(configPath) 18 | if (isConfValid(config)) { 19 | for (const account of config) { 20 | if (account.apiKey.startsWith("ENV:")) { 21 | const envVar = account.apiKey.replace("ENV:", "") 22 | const envValue = process.env[envVar] 23 | if (typeof envValue !== "string" || account.apiKey.length < 10 || account.apiKey.length > 50) { 24 | throw new InvalidEnvVarError(account.name, envVar) 25 | } 26 | account.apiKey = envValue 27 | } 28 | } 29 | return config 30 | } else { 31 | throw new WrongConfigFileError(configPath, isConfValid.errors) 32 | } 33 | } 34 | 35 | export class SdkContext { 36 | public readonly configurationFilePath: string 37 | private _workingDir: string 38 | public get workingDir() { 39 | return this._workingDir 40 | } 41 | private _configuration: ConfType 42 | public get configuration() { 43 | return this._configuration 44 | } 45 | 46 | public reload() { 47 | this._configuration = loadConfig(this.configurationFilePath) 48 | } 49 | 50 | constructor(configPath: string) { 51 | this.configurationFilePath = realpathSync(configPath) 52 | this._workingDir = path.dirname(this.configurationFilePath) 53 | this._configuration = loadConfig(this.configurationFilePath) 54 | } 55 | 56 | public getConfigsForScript(scriptPathToFind: string): IScriptConfig[] { 57 | let realPathToFind = null 58 | try { 59 | realPathToFind = realpathSync(scriptPathToFind).replace(extentionRegexp, "") 60 | } catch { 61 | return [] 62 | } 63 | return this.configuration.reduce((acc, item) => { 64 | const { scripts, ...account } = item 65 | if (!scripts) { 66 | return acc 67 | } 68 | return [ 69 | ...acc, 70 | ...Object.entries(scripts) 71 | .filter(([, scriptPath]) => { 72 | const scriptAbsolutePath = realpathSync(path.join(this.workingDir, scriptPath)).replace( 73 | extentionRegexp, 74 | "", 75 | ) 76 | return scriptAbsolutePath === realPathToFind 77 | }) 78 | .map(([scriptName, scriptPath]) => ({ 79 | account, 80 | scriptPath, 81 | scriptName, 82 | realPath: scriptPathToFind, 83 | })), 84 | ] 85 | }, []) 86 | } 87 | 88 | public async updateScript(updatedPath: string) { 89 | const configs = this.getConfigsForScript(updatedPath) 90 | if (configs.length === 0) { 91 | return false 92 | } 93 | for (const conf of configs) { 94 | if (updatedPath.endsWith(".json") || updatedPath.endsWith(".md")) { 95 | await uploadScriptInfo(conf) 96 | } else { 97 | await uploadScript(conf) 98 | } 99 | } 100 | return true 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/__fixtures__/config.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "library", 4 | "apiKey": "ENV:LIBRARY_API_KEY", 5 | "endpoint": "https://api-staging.phantombuster.io/api/v1", 6 | "scripts": { 7 | "Test.js": "./test.js", 8 | "Test2.js": "./test2.js" 9 | } 10 | } 11 | ] 12 | -------------------------------------------------------------------------------- /src/__fixtures__/test.js: -------------------------------------------------------------------------------- 1 | console.log("test") 2 | -------------------------------------------------------------------------------- /src/__fixtures__/test.json: -------------------------------------------------------------------------------- 1 | { 2 | "slug": "test" 3 | } 4 | -------------------------------------------------------------------------------- /src/__fixtures__/test.md: -------------------------------------------------------------------------------- 1 | markdown test 2 | -------------------------------------------------------------------------------- /src/__fixtures__/test2.js: -------------------------------------------------------------------------------- 1 | console.log("test2") 2 | -------------------------------------------------------------------------------- /src/__fixtures__/test2.json: -------------------------------------------------------------------------------- 1 | { 2 | "slug": "test2" 3 | } 4 | -------------------------------------------------------------------------------- /src/__fixtures__/unconfigured.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phantombuster/sdk/d1427376a774a525ebd9cbe9f5ddadc21f19d524/src/__fixtures__/unconfigured.js -------------------------------------------------------------------------------- /src/__fixtures__/wrongConfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "library", 3 | "apiKey": "ENV:LIBRARY_API_KEY", 4 | "endpoint": "https://api-staging.phantombuster.io/api/v1", 5 | "scripts": { 6 | "Test.js": "./test.js" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/configuration.schema.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | $schema: "http://json-schema.org/draft-04/schema#", 3 | type: "array", 4 | items: { 5 | type: "object", 6 | properties: { 7 | name: { 8 | type: "string", 9 | minLength: 1, 10 | }, 11 | endpoint: { 12 | type: "string", 13 | format: "uri", 14 | }, 15 | apiKey: { 16 | type: "string", 17 | pattern: "^[\\w-:]{5,50}$", 18 | }, 19 | scripts: { 20 | type: "object", 21 | patternProperties: { 22 | "^[\\w\\. -]{1,50}\\.(?:coffee|js)$": { 23 | type: "string", 24 | minLength: 1, 25 | }, 26 | }, 27 | additionalProperties: false, 28 | }, 29 | }, 30 | required: ["name", "apiKey"], 31 | additionalProperties: false, 32 | }, 33 | } as const 34 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | export const defaultEndpoint = "https://phantombuster.com/api/v1" 2 | 3 | export const extentionRegexp = /\.[^.]+$/ 4 | -------------------------------------------------------------------------------- /src/errors.ts: -------------------------------------------------------------------------------- 1 | import type { ValidationError } from "is-my-json-valid" 2 | import { Logger } from "./Logger" 3 | 4 | export abstract class LoggableError extends Error { 5 | abstract logError(): void 6 | } 7 | 8 | export class InvalidEnvVarError extends LoggableError { 9 | constructor(accountName: string, varName: string) { 10 | super(`${accountName}: Environment variable "${varName}" does not contain a valid API key`) 11 | } 12 | logError() { 13 | console.log(this.message) 14 | } 15 | } 16 | 17 | export class WrongConfigFileError extends LoggableError { 18 | constructor( 19 | configPath: string, 20 | private readonly errors: ValidationError[], 21 | ) { 22 | super(`${configPath} is not a correct SDK configuration file`) 23 | } 24 | 25 | logError() { 26 | Logger.log(this.message) 27 | console.log(JSON.stringify(this.errors)) 28 | } 29 | } 30 | 31 | export class SdkFileProcessError extends Error { 32 | constructor( 33 | public readonly filePath: string, 34 | error: unknown, 35 | ) { 36 | const label = filePath.endsWith(".json") || filePath.endsWith(".md") ? "[API store settings] " : "" 37 | super(`${label}${filePath}: ${error}`) 38 | } 39 | } 40 | 41 | export class FileReadError extends SdkFileProcessError { 42 | // 43 | } 44 | export class JSONParseError extends FileReadError { 45 | // 46 | } 47 | -------------------------------------------------------------------------------- /src/fileUpload.ts: -------------------------------------------------------------------------------- 1 | import { extentionRegexp, defaultEndpoint } from "./constants" 2 | import { getJsonFileContent, getFileContent, readFile } from "./files" 3 | import { Logger } from "./Logger" 4 | import { request } from "./requests" 5 | import { IScriptConfig } from "./types" 6 | 7 | function catchAndReturnEmtpy(script: IScriptConfig) { 8 | return function (err: unknown) { 9 | if (err instanceof Error) { 10 | Logger.log(`${script.account.name}: ${err.message}`) 11 | } 12 | return "" 13 | } 14 | } 15 | 16 | export async function uploadScriptInfo(script: IScriptConfig) { 17 | const { account, scriptName, scriptPath, realPath } = script 18 | 19 | const jsonText = await getJsonFileContent(realPath.replace(extentionRegexp, ".json")).catch( 20 | catchAndReturnEmtpy(script), 21 | ) 22 | const mdText = await getFileContent(realPath.replace(extentionRegexp, ".md")).catch(catchAndReturnEmtpy(script)) 23 | 24 | const options = { 25 | json: true, 26 | headers: { 27 | "X-Phantombuster-Key-1": account.apiKey, 28 | }, 29 | } 30 | const payload = { 31 | infoString: jsonText, 32 | markdown: mdText, 33 | } 34 | 35 | try { 36 | const response = await request<{ status: string; message?: string }>( 37 | `${account.endpoint || defaultEndpoint}/store-info/by-name/${scriptName}`, 38 | payload, 39 | options, 40 | ) 41 | if (response.body?.status === "success") { 42 | Logger.log(`${account.name}: [API store settings] ${scriptPath} -> ${scriptName}`) 43 | } else { 44 | Logger.log( 45 | `${account.name}: [API store settings] ${scriptPath}: ${response.body?.status ?? "Error"}: ${response.body?.message ?? "HTTP " + response.statusCode}`, 46 | ) 47 | } 48 | } catch (err) { 49 | Logger.log(`${account.name}: [API store settings] ${scriptPath}: ${err}`) 50 | } 51 | } 52 | 53 | export async function uploadScript({ account, realPath, scriptName, scriptPath }: IScriptConfig) { 54 | try { 55 | const text = await readFile(realPath) 56 | const options = { 57 | json: true, 58 | headers: { 59 | "X-Phantombuster-Key-1": account.apiKey, 60 | }, 61 | } 62 | const payload = { 63 | text: text.toString(), 64 | source: "sdk", 65 | } 66 | try { 67 | const res = await request<{ status: string; message?: string }>( 68 | `${account.endpoint || defaultEndpoint}/script/${scriptName}`, 69 | payload, 70 | options, 71 | ) 72 | if (res.body?.status === "success") { 73 | Logger.log( 74 | `${account.name}: ${scriptPath} -> ${scriptName}${typeof res.body.data === "number" ? " (new script created)" : ""}`, 75 | ) 76 | } else { 77 | Logger.log( 78 | `${account.name}: ${scriptPath}: ${res.body?.status ?? "Error"}: ${res.body?.message ?? "HTTP " + res.statusCode}`, 79 | ) 80 | } 81 | } catch (err) { 82 | Logger.log(`${account.name}: ${scriptPath}: ${err}`) 83 | } 84 | } catch (err) { 85 | Logger.log(`${account.name}: ${scriptPath}: ${err}`) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/files.ts: -------------------------------------------------------------------------------- 1 | import fs, { existsSync } from "fs" 2 | import { FileReadError, JSONParseError } from "./errors" 3 | 4 | export function readFile(filename: string) { 5 | return new Promise((resolve, reject) => { 6 | fs.readFile(filename, (err, value) => { 7 | if (err) { 8 | reject(err) 9 | } else { 10 | resolve(value.toString()) 11 | } 12 | }) 13 | }) 14 | } 15 | 16 | export async function getJsonFileContent(realPath: string): Promise { 17 | const jsonContent = await getFileContent(realPath) 18 | try { 19 | JSON.parse(jsonContent) 20 | return jsonContent 21 | } catch (err) { 22 | throw new JSONParseError(realPath, err) 23 | } 24 | } 25 | 26 | export async function getFileContent(realPath: string): Promise { 27 | if (existsSync(realPath)) { 28 | try { 29 | return await readFile(realPath) 30 | } catch (err) { 31 | if (!(err && typeof err === "object") || !("code" in err) || err.code !== "ENOENT") { 32 | throw new FileReadError(realPath, err) 33 | } 34 | } 35 | } 36 | return "" 37 | } 38 | -------------------------------------------------------------------------------- /src/requests.ts: -------------------------------------------------------------------------------- 1 | import * as needle from "needle" 2 | 3 | type RequestResponse = needle.NeedleResponse & { body?: T } 4 | 5 | export function request(url: string, body: needle.BodyData, options?: needle.NeedleOptions) { 6 | return new Promise>((resolve, reject) => 7 | needle.post(url, body, options, (error, response) => { 8 | if (error) { 9 | reject(error) 10 | } else { 11 | resolve(response) 12 | } 13 | }), 14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /src/sdk.test.ts: -------------------------------------------------------------------------------- 1 | // import * as nodeWatch from "node-watch"s 2 | 3 | import fs from "fs" 4 | 5 | function getSut(sdkPath: string) { 6 | process.env.LIBRARY_API_KEY = "LIBRARY_API_KEY" 7 | 8 | const exitSpy = jest.spyOn(process, "exit").mockImplementation(() => { 9 | return null as never 10 | }) 11 | const postSpy = jest.fn() 12 | jest.mock("needle", () => ({ post: postSpy })) 13 | const watchMock = jest.fn().mockImplementation(() => {}) 14 | const logFunc = jest.spyOn(console, "log").mockImplementation(() => {}) 15 | 16 | jest.mock("node-watch", () => watchMock) 17 | jest.mock("cson", () => ({ 18 | // eslint-disable-next-line @typescript-eslint/no-require-imports 19 | load: jest.fn().mockImplementation((configName) => require(configName)), 20 | })) 21 | jest.spyOn(Date.prototype, "toLocaleTimeString").mockReturnValue("DATE") 22 | 23 | const realFs: typeof import("fs") = jest.requireActual("fs") 24 | 25 | const readFileSpy = jest.spyOn(realFs, "readFile") 26 | 27 | async function launchingScript() { 28 | await import(sdkPath) 29 | await new Promise((resolve) => setTimeout(resolve, 25)) 30 | } 31 | return { 32 | given: { 33 | noLibKeyVariable() { 34 | // process.env.LIBRARY_API_KEY = undefined 35 | delete process.env.LIBRARY_API_KEY 36 | }, 37 | cmdArguments(configFile: string, scriptToProcess?: string[]) { 38 | jest.mock("yargs", () => ({ 39 | argv: { 40 | c: realFs.realpathSync(configFile), 41 | _: scriptToProcess, 42 | }, 43 | })) 44 | }, 45 | fileContentIs(file: string, content: string) { 46 | readFileSpy.mockImplementationOnce((filename, func) => { 47 | if (filename.toString().includes(file)) { 48 | func(null, Buffer.from(content)) 49 | } else { 50 | realFs.readFile(filename, func) 51 | } 52 | }) 53 | }, 54 | fileIsUnreadable(file: string, error: Error) { 55 | readFileSpy.mockImplementationOnce((filename, func) => { 56 | if (filename.toString().includes(file)) { 57 | func(error, Buffer.from("content")) 58 | } else { 59 | realFs.readFile(filename, func) 60 | } 61 | }) 62 | }, 63 | apiAnswer(answer: { 64 | body?: { 65 | status?: string 66 | message?: string 67 | data?: number 68 | } 69 | statusCode: number 70 | }) { 71 | postSpy.mockImplementation((_a, _b, _c, func) => func(null, answer)) 72 | }, 73 | apiError(error: Error) { 74 | postSpy.mockImplementation((_a, _b, _c, func) => func(error)) 75 | }, 76 | }, 77 | when: { 78 | launchingScript, 79 | async updatingFileInWatchMode(fileName: string) { 80 | let promiseToWait = Promise.resolve() 81 | watchMock.mockImplementation((_a, _b, func) => { 82 | try { 83 | promiseToWait = func("update", realFs.realpathSync(fileName)) 84 | } catch { 85 | promiseToWait = func("update", fileName) 86 | } 87 | }) 88 | await launchingScript() 89 | await promiseToWait 90 | }, 91 | async watchThrowAnError(error: Error) { 92 | watchMock.mockImplementationOnce(() => { 93 | throw error 94 | }) 95 | await launchingScript() 96 | }, 97 | }, 98 | then: { 99 | watcherShouldNotLaunch() { 100 | expect(watchMock).not.toHaveBeenCalled() 101 | }, 102 | watcherShouldLaunch() { 103 | expect(watchMock).toHaveBeenCalledTimes(1) 104 | }, 105 | logShouldBeWritten(log: string) { 106 | expect(logFunc).toHaveBeenCalledWith(log) 107 | }, 108 | logShouldNotBeWritten() { 109 | expect(logFunc).not.toHaveBeenCalled() 110 | }, 111 | scriptShouldExitWithError() { 112 | expect(exitSpy).toHaveBeenCalledWith(1) 113 | }, 114 | upsertScriptShouldBeCalledWith(scriptName: string, scriptContent: string) { 115 | expect(postSpy).toHaveBeenCalledWith( 116 | `https://api-staging.phantombuster.io/api/v1/script/${scriptName}`, 117 | { source: "sdk", text: scriptContent }, 118 | { headers: { "X-Phantombuster-Key-1": "LIBRARY_API_KEY" }, json: true }, 119 | expect.anything(), 120 | ) 121 | }, 122 | upsertScriptInfoShouldBeCalledWith( 123 | fileName: string, 124 | { 125 | json, 126 | md, 127 | }: { 128 | json: string 129 | md: string 130 | }, 131 | ) { 132 | expect(postSpy).toHaveBeenCalledWith( 133 | `https://api-staging.phantombuster.io/api/v1/store-info/by-name/${fileName}`, 134 | { infoString: json, markdown: md }, 135 | { headers: { "X-Phantombuster-Key-1": "LIBRARY_API_KEY" }, json: true }, 136 | expect.anything(), 137 | ) 138 | }, 139 | }, 140 | } 141 | } 142 | 143 | beforeEach(() => { 144 | jest.resetModules() 145 | }) 146 | 147 | afterEach(() => { 148 | jest.restoreAllMocks() 149 | }) 150 | 151 | describe.each([ 152 | "../lib/sdk.js", //old sdk 153 | "./sdk.ts", //new sdk 154 | ] as const)("sdk %s", (sdkPath) => { 155 | it("should launch watch if started with only config argument", async () => { 156 | const sut = getSut(sdkPath) 157 | sut.given.cmdArguments("src/__fixtures__/config.json") 158 | await sut.when.launchingScript() 159 | sut.then.watcherShouldLaunch() 160 | }) 161 | 162 | it("should log error and exit if watch throw", async () => { 163 | const sut = getSut(sdkPath) 164 | sut.given.cmdArguments("src/__fixtures__/config.json") 165 | await sut.when.watchThrowAnError(new Error("something went wrong")) 166 | sut.then.logShouldBeWritten("Error: something went wrong") 167 | sut.then.scriptShouldExitWithError() 168 | }) 169 | 170 | it("should log if configuration reload", async () => { 171 | const sut = getSut(sdkPath) 172 | sut.given.cmdArguments("src/__fixtures__/config.json") 173 | await sut.when.updatingFileInWatchMode("src/__fixtures__/config.json") 174 | sut.then.logShouldBeWritten( 175 | "DATE - " + fs.realpathSync("src/__fixtures__/config.json") + ": Configuration reloaded", 176 | ) 177 | }) 178 | 179 | it("should not launch if config as wrong format", async () => { 180 | const sut = getSut(sdkPath) 181 | sut.given.cmdArguments("src/__fixtures__/wrongConfig.json") 182 | await sut.when.launchingScript() 183 | sut.then.logShouldBeWritten( 184 | "DATE - " + 185 | fs.realpathSync("src/__fixtures__/wrongConfig.json") + 186 | " is not a correct SDK configuration file", 187 | ) 188 | }) 189 | 190 | it("should not launch watch if started with files as arguments", async () => { 191 | const sut = getSut(sdkPath) 192 | sut.given.cmdArguments("src/__fixtures__/config.json", [ 193 | "src/__fixtures__/unconfigured.js", 194 | "src/__fixtures__/test.js", 195 | ]) 196 | sut.given.apiAnswer({ 197 | statusCode: 200, 198 | body: { 199 | status: "success", 200 | }, 201 | }) 202 | await sut.when.launchingScript() 203 | 204 | sut.then.watcherShouldNotLaunch() 205 | sut.then.logShouldBeWritten("DATE - src/__fixtures__/unconfigured.js: Not found in configuration") 206 | sut.then.logShouldBeWritten("DATE - library: ./test.js -> Test.js") 207 | }) 208 | 209 | it("should not launch with wrong env var", async () => { 210 | const sut = getSut(sdkPath) 211 | sut.given.noLibKeyVariable() 212 | sut.given.cmdArguments("src/__fixtures__/config.json") 213 | await sut.when.launchingScript() 214 | sut.then.logShouldBeWritten('library: Environment variable "LIBRARY_API_KEY" does not contain a valid API key') 215 | sut.then.scriptShouldExitWithError() 216 | }) 217 | 218 | describe("api calls", () => { 219 | it("should call the api for correct js file", async () => { 220 | const sut = getSut(sdkPath) 221 | sut.given.cmdArguments("src/__fixtures__/config.json") 222 | sut.given.apiAnswer({ 223 | body: { 224 | status: "success", 225 | message: "", 226 | // data: 2, 227 | }, 228 | statusCode: 200, 229 | }) 230 | 231 | await sut.when.updatingFileInWatchMode("src/__fixtures__/test.js") 232 | 233 | sut.then.upsertScriptShouldBeCalledWith("Test.js", 'console.log("test")\n') 234 | }) 235 | 236 | it("should call the api for correct json file", async () => { 237 | const sut = getSut(sdkPath) 238 | sut.given.cmdArguments("src/__fixtures__/config.json") 239 | sut.given.apiAnswer({ 240 | body: { 241 | status: "success", 242 | message: "", 243 | // data: 2, 244 | }, 245 | statusCode: 200, 246 | }) 247 | 248 | await sut.when.updatingFileInWatchMode("src/__fixtures__/test.json") 249 | 250 | sut.then.upsertScriptInfoShouldBeCalledWith("Test.js", { 251 | json: '{\n\t"slug": "test"\n}\n', 252 | md: "markdown test\n", 253 | }) 254 | }) 255 | 256 | it("should call the api for correct json file without markdown", async () => { 257 | const sut = getSut(sdkPath) 258 | sut.given.cmdArguments("src/__fixtures__/config.json") 259 | sut.given.apiAnswer({ 260 | body: { 261 | status: "success", 262 | message: "", 263 | // data: 2, 264 | }, 265 | statusCode: 200, 266 | }) 267 | 268 | await sut.when.updatingFileInWatchMode("src/__fixtures__/test2.json") 269 | 270 | sut.then.upsertScriptInfoShouldBeCalledWith("Test2.js", { 271 | json: '{\n\t"slug": "test2"\n}\n', 272 | md: "", 273 | }) 274 | }) 275 | 276 | it("should call the api for correct md file", async () => { 277 | const sut = getSut(sdkPath) 278 | sut.given.cmdArguments("src/__fixtures__/config.json") 279 | sut.given.apiAnswer({ 280 | body: { 281 | status: "success", 282 | message: "", 283 | // data: 2, 284 | }, 285 | statusCode: 200, 286 | }) 287 | 288 | await sut.when.updatingFileInWatchMode("src/__fixtures__/test.md") 289 | 290 | sut.then.upsertScriptInfoShouldBeCalledWith("Test.js", { 291 | json: '{\n\t"slug": "test"\n}\n', 292 | md: "markdown test\n", 293 | }) 294 | }) 295 | }) 296 | 297 | describe("script file change", () => { 298 | const file = "src/__fixtures__/test.js" 299 | 300 | it("should not log if script does not exists", async () => { 301 | const sut = getSut(sdkPath) 302 | sut.given.cmdArguments("src/__fixtures__/config.json") 303 | sut.given.apiAnswer({ 304 | body: { 305 | status: "success", 306 | message: "", 307 | // data: 2, 308 | }, 309 | statusCode: 200, 310 | }) 311 | await sut.when.updatingFileInWatchMode("unconfigured.js") 312 | sut.then.logShouldNotBeWritten() 313 | }) 314 | 315 | it("should log if script is updated", async () => { 316 | const sut = getSut(sdkPath) 317 | sut.given.cmdArguments("src/__fixtures__/config.json") 318 | sut.given.apiAnswer({ 319 | body: { 320 | status: "success", 321 | message: "", 322 | // data: 2, 323 | }, 324 | statusCode: 200, 325 | }) 326 | await sut.when.updatingFileInWatchMode(file) 327 | sut.then.logShouldBeWritten("DATE - library: ./test.js -> Test.js") 328 | }) 329 | 330 | it("should log if script is created", async () => { 331 | const sut = getSut(sdkPath) 332 | sut.given.cmdArguments("src/__fixtures__/config.json") 333 | sut.given.apiAnswer({ 334 | body: { 335 | status: "success", 336 | message: "", 337 | data: 2, 338 | }, 339 | statusCode: 200, 340 | }) 341 | await sut.when.updatingFileInWatchMode(file) 342 | sut.then.logShouldBeWritten("DATE - library: ./test.js -> Test.js (new script created)") 343 | }) 344 | 345 | it("should log status code as error if no message", async () => { 346 | const sut = getSut(sdkPath) 347 | sut.given.cmdArguments("src/__fixtures__/config.json") 348 | sut.given.apiAnswer({ 349 | statusCode: 412, 350 | }) 351 | await sut.when.updatingFileInWatchMode(file) 352 | sut.then.logShouldBeWritten("DATE - library: ./test.js: Error: HTTP 412") 353 | }) 354 | 355 | it("should log message error", async () => { 356 | const sut = getSut(sdkPath) 357 | sut.given.cmdArguments("src/__fixtures__/config.json") 358 | sut.given.apiAnswer({ 359 | body: { 360 | message: "not found", 361 | }, 362 | statusCode: 412, 363 | }) 364 | await sut.when.updatingFileInWatchMode(file) 365 | sut.then.logShouldBeWritten("DATE - library: ./test.js: Error: not found") 366 | }) 367 | 368 | it("should log message error with status if exists", async () => { 369 | const sut = getSut(sdkPath) 370 | sut.given.cmdArguments("src/__fixtures__/config.json") 371 | sut.given.apiAnswer({ 372 | body: { 373 | message: "not found", 374 | status: "status", 375 | }, 376 | statusCode: 412, 377 | }) 378 | await sut.when.updatingFileInWatchMode(file) 379 | sut.then.logShouldBeWritten("DATE - library: ./test.js: status: not found") 380 | }) 381 | 382 | it("should log message error if sdkrequest failed", async () => { 383 | const sut = getSut(sdkPath) 384 | sut.given.cmdArguments("src/__fixtures__/config.json") 385 | sut.given.apiError(new Error("request failed")) 386 | await sut.when.updatingFileInWatchMode(file) 387 | sut.then.logShouldBeWritten("DATE - library: ./test.js: Error: request failed") 388 | }) 389 | 390 | it("should log message error if reading file failed", async () => { 391 | const sut = getSut(sdkPath) 392 | sut.given.cmdArguments("src/__fixtures__/config.json") 393 | sut.given.apiError(new Error("request failed")) 394 | sut.given.fileIsUnreadable(file, new Error("error while reading file")) 395 | 396 | await sut.when.updatingFileInWatchMode(file) 397 | sut.then.logShouldBeWritten("DATE - library: ./test.js: Error: error while reading file") 398 | }) 399 | }) 400 | 401 | describe("json file change", () => { 402 | const file = "src/__fixtures__/test.json" 403 | 404 | it("should not log if file does not exists", async () => { 405 | const sut = getSut(sdkPath) 406 | sut.given.cmdArguments("src/__fixtures__/config.json") 407 | sut.given.apiAnswer({ 408 | body: { 409 | status: "success", 410 | message: "", 411 | // data: 2, 412 | }, 413 | statusCode: 200, 414 | }) 415 | sut.given.fileContentIs(file, "{}") 416 | await sut.when.updatingFileInWatchMode("toto.json") 417 | sut.then.logShouldNotBeWritten() 418 | }) 419 | 420 | it("should log if file is updated", async () => { 421 | const sut = getSut(sdkPath) 422 | sut.given.cmdArguments("src/__fixtures__/config.json") 423 | sut.given.apiAnswer({ 424 | body: { 425 | status: "success", 426 | message: "", 427 | // data: 2, 428 | }, 429 | statusCode: 200, 430 | }) 431 | await sut.when.updatingFileInWatchMode(file) 432 | sut.then.logShouldBeWritten("DATE - library: [API store settings] ./test.js -> Test.js") 433 | }) 434 | 435 | it("should log if script is created", async () => { 436 | const sut = getSut(sdkPath) 437 | sut.given.cmdArguments("src/__fixtures__/config.json") 438 | sut.given.apiAnswer({ 439 | body: { 440 | status: "success", 441 | message: "", 442 | data: 2, 443 | }, 444 | statusCode: 200, 445 | }) 446 | await sut.when.updatingFileInWatchMode(file) 447 | sut.then.logShouldBeWritten("DATE - library: [API store settings] ./test.js -> Test.js") 448 | }) 449 | 450 | it("should log status code as error if no message", async () => { 451 | const sut = getSut(sdkPath) 452 | sut.given.cmdArguments("src/__fixtures__/config.json") 453 | sut.given.apiAnswer({ 454 | statusCode: 412, 455 | }) 456 | await sut.when.updatingFileInWatchMode(file) 457 | sut.then.logShouldBeWritten("DATE - library: [API store settings] ./test.js: Error: HTTP 412") 458 | }) 459 | 460 | it("should log message error", async () => { 461 | const sut = getSut(sdkPath) 462 | sut.given.cmdArguments("src/__fixtures__/config.json") 463 | sut.given.apiAnswer({ 464 | body: { 465 | message: "not found", 466 | }, 467 | statusCode: 412, 468 | }) 469 | await sut.when.updatingFileInWatchMode(file) 470 | sut.then.logShouldBeWritten("DATE - library: [API store settings] ./test.js: Error: not found") 471 | }) 472 | 473 | it("should log message error with status if exists", async () => { 474 | const sut = getSut(sdkPath) 475 | sut.given.cmdArguments("src/__fixtures__/config.json") 476 | sut.given.apiAnswer({ 477 | body: { 478 | message: "not found", 479 | status: "status", 480 | }, 481 | statusCode: 412, 482 | }) 483 | await sut.when.updatingFileInWatchMode(file) 484 | sut.then.logShouldBeWritten("DATE - library: [API store settings] ./test.js: status: not found") 485 | }) 486 | 487 | it("should log message error if sdkrequest failed", async () => { 488 | const sut = getSut(sdkPath) 489 | sut.given.cmdArguments("src/__fixtures__/config.json") 490 | sut.given.apiError(new Error("request failed")) 491 | await sut.when.updatingFileInWatchMode(file) 492 | sut.then.logShouldBeWritten("DATE - library: [API store settings] ./test.js: Error: request failed") 493 | }) 494 | 495 | it("should log message error if reading file failed", async () => { 496 | const sut = getSut(sdkPath) 497 | sut.given.cmdArguments("src/__fixtures__/config.json") 498 | sut.given.apiAnswer({ 499 | body: { 500 | status: "success", 501 | }, 502 | statusCode: 200, 503 | }) 504 | sut.given.apiAnswer({ 505 | body: { 506 | status: "success", 507 | message: "", 508 | }, 509 | statusCode: 200, 510 | }) 511 | sut.given.fileIsUnreadable(file, new Error("error while reading file")) 512 | await sut.when.updatingFileInWatchMode(file) 513 | sut.then.logShouldBeWritten( 514 | "DATE - library: [API store settings] " + fs.realpathSync(file) + ": Error: error while reading file", 515 | ) 516 | }) 517 | 518 | it("should log message error if raising 'ENOENT' error", async () => { 519 | const sut = getSut(sdkPath) 520 | sut.given.cmdArguments("src/__fixtures__/config.json") 521 | sut.given.apiAnswer({ 522 | body: { 523 | status: "success", 524 | }, 525 | statusCode: 200, 526 | }) 527 | sut.given.apiAnswer({ 528 | body: { 529 | status: "success", 530 | message: "", 531 | }, 532 | statusCode: 200, 533 | }) 534 | const error = new Error("error while reading file") as Error & { code: string } 535 | error.code = "ENOENT" 536 | sut.given.fileIsUnreadable(file, error) 537 | await sut.when.updatingFileInWatchMode(file) 538 | sut.then.logShouldBeWritten("DATE - library: [API store settings] ./test.js -> Test.js") 539 | }) 540 | 541 | it("should log message error if file does not contains JSON", async () => { 542 | const sut = getSut(sdkPath) 543 | sut.given.cmdArguments("src/__fixtures__/config.json") 544 | sut.given.apiError(new Error("request failed")) 545 | sut.given.fileContentIs(file, "toto") 546 | await sut.when.updatingFileInWatchMode(file) 547 | sut.then.logShouldBeWritten( 548 | "DATE - library: [API store settings] " + 549 | fs.realpathSync(file) + 550 | ": SyntaxError: Unexpected token 'o', \"toto\" is not valid JSON", 551 | ) 552 | }) 553 | }) 554 | 555 | describe("md file change", () => { 556 | const file = "src/__fixtures__/test.md" 557 | 558 | it("should not log if file does not exists", async () => { 559 | const sut = getSut(sdkPath) 560 | sut.given.cmdArguments("src/__fixtures__/config.json") 561 | sut.given.apiAnswer({ 562 | body: { 563 | status: "success", 564 | message: "", 565 | // data: 2, 566 | }, 567 | statusCode: 200, 568 | }) 569 | sut.given.fileContentIs(file, "{}") 570 | await sut.when.updatingFileInWatchMode("toto.json") 571 | sut.then.logShouldNotBeWritten() 572 | }) 573 | 574 | it("should log if file is updated", async () => { 575 | const sut = getSut(sdkPath) 576 | sut.given.cmdArguments("src/__fixtures__/config.json") 577 | sut.given.apiAnswer({ 578 | body: { 579 | status: "success", 580 | message: "", 581 | // data: 2, 582 | }, 583 | statusCode: 200, 584 | }) 585 | 586 | await sut.when.updatingFileInWatchMode(file) 587 | 588 | sut.then.logShouldBeWritten("DATE - library: [API store settings] ./test.js -> Test.js") 589 | }) 590 | 591 | it("should log if script is created", async () => { 592 | const sut = getSut(sdkPath) 593 | sut.given.cmdArguments("src/__fixtures__/config.json") 594 | sut.given.apiAnswer({ 595 | body: { 596 | status: "success", 597 | message: "", 598 | data: 2, 599 | }, 600 | statusCode: 200, 601 | }) 602 | 603 | await sut.when.updatingFileInWatchMode(file) 604 | 605 | sut.then.logShouldBeWritten("DATE - library: [API store settings] ./test.js -> Test.js") 606 | }) 607 | 608 | it("should log status code as error if no message", async () => { 609 | const sut = getSut(sdkPath) 610 | sut.given.cmdArguments("src/__fixtures__/config.json") 611 | sut.given.apiAnswer({ 612 | statusCode: 412, 613 | }) 614 | 615 | await sut.when.updatingFileInWatchMode(file) 616 | 617 | sut.then.logShouldBeWritten("DATE - library: [API store settings] ./test.js: Error: HTTP 412") 618 | }) 619 | 620 | it("should log message error", async () => { 621 | const sut = getSut(sdkPath) 622 | sut.given.cmdArguments("src/__fixtures__/config.json") 623 | sut.given.apiAnswer({ 624 | body: { 625 | message: "not found", 626 | }, 627 | statusCode: 412, 628 | }) 629 | 630 | await sut.when.updatingFileInWatchMode(file) 631 | 632 | sut.then.logShouldBeWritten("DATE - library: [API store settings] ./test.js: Error: not found") 633 | }) 634 | 635 | it("should log message error with status if exists", async () => { 636 | const sut = getSut(sdkPath) 637 | sut.given.cmdArguments("src/__fixtures__/config.json") 638 | sut.given.apiAnswer({ 639 | body: { 640 | message: "not found", 641 | status: "status", 642 | }, 643 | statusCode: 412, 644 | }) 645 | 646 | await sut.when.updatingFileInWatchMode(file) 647 | 648 | sut.then.logShouldBeWritten("DATE - library: [API store settings] ./test.js: status: not found") 649 | }) 650 | 651 | it("should log message error if sdkrequest failed", async () => { 652 | const sut = getSut(sdkPath) 653 | sut.given.cmdArguments("src/__fixtures__/config.json") 654 | sut.given.apiError(new Error("request failed")) 655 | 656 | await sut.when.updatingFileInWatchMode(file) 657 | 658 | sut.then.logShouldBeWritten("DATE - library: [API store settings] ./test.js: Error: request failed") 659 | }) 660 | 661 | it("should log message error if raising 'ENOENT' error", async () => { 662 | const sut = getSut(sdkPath) 663 | sut.given.cmdArguments("src/__fixtures__/config.json") 664 | sut.given.apiAnswer({ 665 | body: { 666 | status: "success", 667 | }, 668 | statusCode: 200, 669 | }) 670 | sut.given.apiAnswer({ 671 | body: { 672 | status: "success", 673 | message: "", 674 | }, 675 | statusCode: 200, 676 | }) 677 | const error = new Error("error while reading file") as Error & { code: string } 678 | error.code = "ENOENT" 679 | sut.given.fileIsUnreadable(file, error) 680 | await sut.when.updatingFileInWatchMode(file) 681 | sut.then.logShouldBeWritten("DATE - library: [API store settings] ./test.js -> Test.js") 682 | }) 683 | 684 | it("should log message error if reading file failed", async () => { 685 | const sut = getSut(sdkPath) 686 | sut.given.cmdArguments("src/__fixtures__/config.json") 687 | const error = new Error("error while reading file") as Error & { code: string } 688 | error.code = "ENOENT" 689 | sut.given.fileIsUnreadable(file.replace(".md", ".json"), error) 690 | sut.given.fileIsUnreadable(file, new Error("error while reading file")) 691 | sut.given.apiAnswer({ 692 | body: { 693 | status: "success", 694 | }, 695 | statusCode: 200, 696 | }) 697 | 698 | await sut.when.updatingFileInWatchMode(file) 699 | 700 | sut.then.logShouldBeWritten( 701 | "DATE - library: [API store settings] " + fs.realpathSync(file) + ": Error: error while reading file", 702 | ) 703 | }) 704 | }) 705 | }) 706 | -------------------------------------------------------------------------------- /src/sdk.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import watch from "node-watch" 4 | import { argv } from "yargs" 5 | 6 | import { Logger } from "./Logger" 7 | import { LoggableError } from "./errors" 8 | import { SdkContext } from "./SdkContext" 9 | 10 | async function main() { 11 | const args = await argv 12 | const CONFIG_PATH = (args.c as string) || "phantombuster.cson" 13 | const ctx = new SdkContext(CONFIG_PATH) 14 | if (args._?.length) { 15 | for (const script of args._) { 16 | if (!(await ctx.updateScript(script as string))) { 17 | Logger.log(`${script}: Not found in configuration`) 18 | } 19 | } 20 | } else { 21 | const watchOptions = { 22 | recursive: true, 23 | filter: (f: string) => !/node_modules/.test(f), 24 | } 25 | watch(ctx.workingDir, watchOptions, async (event, updatedPath) => { 26 | if (event === "update") { 27 | if (updatedPath === ctx.configurationFilePath) { 28 | ctx.reload() 29 | Logger.log(`${updatedPath}: Configuration reloaded`) 30 | } else { 31 | await ctx.updateScript(updatedPath) 32 | } 33 | } 34 | }) 35 | } 36 | } 37 | 38 | main().catch((error) => { 39 | if (error instanceof LoggableError) { 40 | error.logError() 41 | } else { 42 | console.log(error.toString()) 43 | } 44 | process.exit(1) 45 | }) 46 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import type { AsTyped } from "as-typed" 2 | import type conf from "./configuration.schema" 3 | 4 | // cast is needed because AsTyped does not manage pattern properties 5 | export type ConfType = Array< 6 | AsTyped[number] & { 7 | scripts?: Record 8 | } 9 | > 10 | 11 | export interface IAccount { 12 | name: string 13 | apiKey: string 14 | endpoint?: string 15 | } 16 | 17 | export interface IScriptConfig { 18 | account: IAccount 19 | scriptPath: string 20 | scriptName: string 21 | realPath: string 22 | } 23 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src", "./*.ts", ".eslintrc.js"], 3 | "compilerOptions": { 4 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 5 | 6 | /* Projects */ 7 | // "incremental": true, /* Enable incremental compilation */ 8 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 9 | // "tsBuildInfoFile": "./", /* Specify the folder for .tsbuildinfo incremental compilation files. */ 10 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */ 11 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 12 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 13 | 14 | /* Language and Environment */ 15 | "target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, 16 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 17 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 18 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ 19 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 20 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */ 21 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 22 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */ 23 | // "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */ 24 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 25 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 26 | 27 | /* Modules */ 28 | "module": "commonjs" /* Specify what module code is generated. */, 29 | // "rootDir": "./", /* Specify the root folder within your source files. */ 30 | // "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ 31 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 32 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 33 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 34 | // "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */ 35 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 36 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 37 | "resolveJsonModule": true /* Enable importing .json files */, 38 | // "noResolve": true, /* Disallow `import`s, `require`s or ``s from expanding the number of files TypeScript should add to a project. */ 39 | 40 | /* JavaScript Support */ 41 | "allowJs": true /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */, 42 | "checkJs": true /* Enable error reporting in type-checked JavaScript files. */, 43 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */ 44 | 45 | /* Emit */ 46 | "declaration": true /* Generate .d.ts files from TypeScript and JavaScript files in your project. */, 47 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 48 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 49 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 50 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */ 51 | // "outDir": "./build" /* Specify an output folder for all emitted files. */, 52 | // "removeComments": true, /* Disable emitting comments. */ 53 | "noEmit": true /* Disable emitting files from a compilation. */, 54 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 55 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */ 56 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 57 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 58 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 59 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 60 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 61 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 62 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 63 | // "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */ 64 | // "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */ 65 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 66 | // "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */ 67 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 68 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 69 | 70 | /* Interop Constraints */ 71 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 72 | "allowSyntheticDefaultImports": true /* Allow 'import x from y' when a module doesn't have a default export. */, 73 | "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */, 74 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 75 | "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, 76 | 77 | /* Type Checking */ 78 | "strict": true /* Enable all strict type-checking options. */, 79 | "noImplicitAny": true /* Enable error reporting for expressions and declarations with an implied `any` type.. */, 80 | "strictNullChecks": true /* When type checking, take into account `null` and `undefined`. */, 81 | "strictFunctionTypes": true /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */, 82 | "strictBindCallApply": true /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */, 83 | "strictPropertyInitialization": true /* Check for class properties that are declared but not set in the constructor. */, 84 | "noImplicitThis": true /* Enable error reporting when `this` is given the type `any`. */, 85 | "useUnknownInCatchVariables": true /* Type catch clause variables as 'unknown' instead of 'any'. */, 86 | "alwaysStrict": true /* Ensure 'use strict' is always emitted. */, 87 | "noUnusedLocals": true /* Enable error reporting when a local variables aren't read. */, 88 | "noUnusedParameters": true /* Raise an error when a function parameter isn't read */ 89 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 90 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 91 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 92 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 93 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 94 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */ 95 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 96 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 97 | 98 | /* Completeness */ 99 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 100 | // "skipLibCheck": true /* Skip type checking all .d.ts files. */ 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /tsconfig.prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["src/*.test.ts"], 4 | "include": ["src/*.ts"], 5 | 6 | "compilerOptions": { 7 | "outDir": "./build", 8 | "noEmit": false 9 | } 10 | } 11 | --------------------------------------------------------------------------------