Skip to content

Commit 353d2a6

Browse files
committed
selection, configuration and settings of CLI local development
1 parent acbec0b commit 353d2a6

File tree

6 files changed

+241
-2
lines changed

6 files changed

+241
-2
lines changed

src/SDK/Language/CLI.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,11 @@ public function getFiles(): array
182182
'destination' => 'lib/commands/push.js',
183183
'template' => 'cli/lib/commands/push.js.twig',
184184
],
185+
[
186+
'scope' => 'default',
187+
'destination' => 'lib/commands/run.js',
188+
'template' => 'cli/lib/commands/run.js.twig',
189+
],
185190
[
186191
'scope' => 'service',
187192
'destination' => '/lib/commands/{{service.name | caseDash}}.js',

templates/cli/index.js.twig

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ const { client } = require("./lib/commands/generic");
1313
{% if sdk.test != "true" %}
1414
const { login, logout, whoami } = require("./lib/commands/generic");
1515
const { pull } = require("./lib/commands/pull");
16+
const { run } = require("./lib/commands/run");
1617
const { push } = require("./lib/commands/push");
1718
{% endif %}
1819
{% for service in spec.services %}
@@ -40,6 +41,7 @@ program
4041
.addCommand(login)
4142
.addCommand(pull)
4243
.addCommand(push)
44+
.addCommand(run)
4345
.addCommand(logout)
4446
{% endif %}
4547
{% for service in spec.services %}
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
const inquirer = require("inquirer");
2+
const { Command } = require("commander");
3+
const { localConfig, globalConfig } = require("../config");
4+
const { paginate } = require('../paginate');
5+
const { questionsRunFunctions } = require("../questions");
6+
const { actionRunner, success, log, error, commandDescriptions, drawTable } = require("../parser");
7+
const { systemHasCommand, isPortTaken } = require('../utils');
8+
9+
const systemTools = {
10+
'node': {
11+
commands: [
12+
{ command: "node", docs: "https://nodejs.org/en/download/package-manager" },
13+
{ command: "npm", docs: "https://nodejs.org/en/download/package-manager" },
14+
],
15+
dependencyFiles: [ "package.json", "package-lock.json" ]
16+
},
17+
// TODO: Add all runtime needs
18+
};
19+
20+
const runFunction = async ({ port, engine, functionId } = {}) => {
21+
// Selection
22+
if(!functionId) {
23+
const answers = await inquirer.prompt(questionsRunFunctions[0]);
24+
functionId = answers.function;
25+
}
26+
27+
const functions = localConfig.getFunctions();
28+
const func = functions.find((f) => f.$id === functionId);
29+
if (!func) {
30+
throw new Error("Function '" + functionId + "' not found.")
31+
}
32+
33+
// Configuration: Port
34+
if(port) {
35+
port = +port;
36+
}
37+
38+
if(isNaN(port)) {
39+
port = null;
40+
}
41+
42+
if(port) {
43+
const taken = await isPortTaken(port);
44+
45+
if(taken) {
46+
log(`Port ${port} is already used.`);
47+
port = null;
48+
}
49+
}
50+
51+
if(!port) {
52+
const answers = await inquirer.prompt(questionsRunFunctions[1]);
53+
port = answers.port;
54+
}
55+
56+
// Configuration: Engine
57+
if(engine !== "system" && engine !== "docker") {
58+
engine = null;
59+
}
60+
61+
if(!engine) {
62+
const answers = await inquirer.prompt(questionsRunFunctions[2]);
63+
engine = answers.engine;
64+
}
65+
66+
if(engine === 'docker') {
67+
log('💡 Hint: Using system is faster, but using Docker simulates the production environment precisely.');
68+
69+
if(!systemHasCommand('docker')) {
70+
return error("Please install Docker first: https://docs.docker.com/engine/install/");
71+
}
72+
} else if(engine === 'system') {
73+
log('💡 Hint: Docker simulates the production environment precisely, but using system is faster');
74+
75+
const runtimeName = func.runtime.split('-')[0];
76+
const tool = systemTools[runtimeName];
77+
78+
for(const command of tool.commands) {
79+
if(!systemHasCommand(command.command)) {
80+
return error(`Your system is missing command "${command.command}". Please install it first: ${command.docs}`);
81+
}
82+
}
83+
}
84+
85+
// Settings
86+
const settings = {
87+
runtime: func.runtime,
88+
entrypoint: func.entrypoint,
89+
path: func.path,
90+
commands: func.commands,
91+
};
92+
log("Local function configuration:");
93+
drawTable([settings]);
94+
log('If you wish to change local settings, update appwrite.json file and rerun the command. To deploy the function, run: appwrite push function');
95+
96+
childProcess.execSync('where ' + command, { stdio: 'pipe' })
97+
98+
}
99+
100+
const run = new Command("run")
101+
.alias("dev")
102+
.description(commandDescriptions['run'])
103+
.configureHelp({
104+
helpWidth: process.stdout.columns || 80
105+
})
106+
.action(actionRunner(async (_options, command) => {
107+
command.help();
108+
}));
109+
110+
run
111+
.command("function")
112+
.alias("functions")
113+
.description("Run functions in the current directory.")
114+
.option(`--functionId <functionId>`, `Function ID`)
115+
.option(`--port <port>`, `Local port`)
116+
.option(`--engine <engine>`, `Local engine, "system" or "docker"`)
117+
.action(actionRunner(runFunction));
118+
119+
module.exports = {
120+
run
121+
}

templates/cli/lib/parser.js.twig

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@ const commandDescriptions = {
157157
"avatars": `The avatars command aims to help you complete everyday tasks related to your app image, icons, and avatars.`,
158158
"databases": `The databases command allows you to create structured collections of documents, query and filter lists of documents.`,
159159
"push": `The push command provides a convenient wrapper for pushing your functions, collections, buckets, teams and messaging.`,
160+
"run": `The dev command allows you to run project locally to allow easy development and quick debugging.`,
160161
"functions": `The functions command allows you view, create and manage your Cloud Functions.`,
161162
"health": `The health command allows you to both validate and monitor your {{ spec.title|caseUcfirst }} server's health.`,
162163
"pull": `The pull command helps you pull your {{ spec.title|caseUcfirst }} project, functions, collections, buckets, teams and messaging`,

templates/cli/lib/questions.js.twig

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ const { accountListMfaFactors } = require("./commands/account");
66
const { sdkForConsole } = require("./sdks");
77
const { validateRequired } = require("./validations");
88
const { paginate } = require('./paginate');
9+
const { isPortTaken } = require('./utils');
910

1011
const { databasesList } = require('./commands/databases');
1112
const JSONbig = require("json-bigint")({ storeAsString: false });
@@ -515,6 +516,76 @@ const questionsMfaChallenge = [
515516
}
516517
];
517518

519+
const questionsRunFunctions = [
520+
{
521+
type: "list",
522+
name: "function",
523+
message: "Which function would you like to develop locally?",
524+
validate: (value) => validateRequired('function', value),
525+
choices: () => {
526+
let functions = localConfig.getFunctions();
527+
if (functions.length === 0) {
528+
throw new Error("No functions found in the current directory.");
529+
}
530+
let choices = functions.map((func, idx) => {
531+
return {
532+
name: `${func.name} (${func.$id})`,
533+
value: func.$id
534+
}
535+
})
536+
return choices;
537+
}
538+
},
539+
{
540+
type: "number",
541+
name: "port",
542+
message: 'Which port would you like function to listen on?',
543+
default: async () => {
544+
let port = 3000;
545+
while(port < 3100) {
546+
const taken = await isPortTaken(port);
547+
if(!taken) {
548+
return port;
549+
}
550+
551+
port++;
552+
}
553+
554+
return 3000;
555+
},
556+
validate: function(value) {
557+
const done = this.async();
558+
559+
(async () => {
560+
const taken = await isPortTaken(value);
561+
562+
if(taken) {
563+
throw Error(`Port ${value} is taken. Pick another one.`);
564+
}
565+
})().then(() => {
566+
done(null, true);
567+
}).catch((err) => {
568+
done(err.message);
569+
});
570+
},
571+
},
572+
{
573+
type: "list",
574+
name: "engine",
575+
message: "Which engine would you like to use?",
576+
choices: [
577+
{
578+
name: "Docker",
579+
value: "docker",
580+
},
581+
{
582+
name: "System",
583+
value: "system",
584+
},
585+
],
586+
},
587+
];
588+
518589
module.exports = {
519590
questionsPullProject,
520591
questionsLogin,
@@ -528,5 +599,6 @@ module.exports = {
528599
questionsPushTeams,
529600
questionsGetEntrypoint,
530601
questionsListFactors,
531-
questionsMfaChallenge
602+
questionsMfaChallenge,
603+
questionsRunFunctions
532604
};

templates/cli/lib/utils.js.twig

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
const fs = require("fs");
22
const path = require("path");
3+
const net = require("net");
4+
const childProcess = require('child_process');
35

46
function getAllFiles(folder) {
57
const files = [];
@@ -14,6 +16,42 @@ function getAllFiles(folder) {
1416
return files;
1517
}
1618

19+
async function isPortTaken(port) {
20+
const taken = await new Promise((res, rej) => {
21+
const tester = net.createServer()
22+
.once('error', function (err) {
23+
if (err.code != 'EADDRINUSE') return rej(err)
24+
res(true)
25+
})
26+
.once('listening', function() {
27+
tester.once('close', function() { res(false) })
28+
.close()
29+
})
30+
.listen(port);
31+
});
32+
33+
return taken;
34+
}
35+
36+
function systemHasCommand(command) {
37+
const isUsingWindows = process.platform == 'win32'
38+
39+
try {
40+
if(isUsingWindows) {
41+
childProcess.execSync('where ' + command, { stdio: 'pipe' })
42+
} else {
43+
childProcess.execSync(`[[ $(${command} --version) ]] || { exit 1; } && echo "OK"`, { stdio: 'pipe', shell: '/bin/bash' });
44+
}
45+
} catch (error) {
46+
console.log(error);
47+
return false;
48+
}
49+
50+
return true;
51+
}
52+
1753
module.exports = {
18-
getAllFiles
54+
getAllFiles,
55+
isPortTaken,
56+
systemHasCommand
1957
};

0 commit comments

Comments
 (0)