Skip to content

Commit

Permalink
Merge pull request #32 from golemfactory/feature/JST-424/New-command
Browse files Browse the repository at this point in the history
feat(new app command): implemented new app creation command from template
  • Loading branch information
pgrzy-golem authored Sep 19, 2023
2 parents d55b824 + b6f86c1 commit 3d2d0e0
Show file tree
Hide file tree
Showing 10 changed files with 300 additions and 7 deletions.
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,7 @@ dist/
.DS_Store

manifest.json
*.sig
*.sig

data/project-templates/**/package-lock.json
data/project-templates/**/node_modules
6 changes: 6 additions & 0 deletions .npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
.*
tsconfig.json
node_modules/
data/**/package-lock.json
data/**/node_modules/
src/
25 changes: 25 additions & 0 deletions data/project-templates/js-node/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"name": "nodejs-golem-app",
"version": "1.0.0",
"description": "NodeJS script using Golem Network",
"main": "src/index.js",
"type": "module",
"scripts": {
"run": "node src/index.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"keywords": [
"golem",
"network",
"application"
],
"dependencies": {
"@golem-sdk/golem-js": "^0.11.2",
"dotenv": "^16.3.1"
},
"devDependencies": {
"@golem-sdk/cli": "^1.0.0-beta.1"
}
}
39 changes: 39 additions & 0 deletions data/project-templates/js-node/src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import * as dotenv from "dotenv";
import { LogLevel, ProposalFilters, TaskExecutor } from "@golem-sdk/golem-js";

dotenv.config();

(async function main() {
const executor = await TaskExecutor.create({
// What do you want to run
package: "golem/node:20-alpine",

// How much you wish to spend
budget: 0.5,
proposalFilter: ProposalFilters.limitPriceFilter({
start: 0.1,
cpuPerSec: 0.1 / 3600,
envPerSec: 0.1 / 3600,
}),

// Where you want to spend
payment: {
network: "polygon",
},

// Control the execution of tasks
maxTaskRetries: 0,

// Useful for debugging
logLevel: LogLevel.Info,
taskTimeout: 5 * 60 * 1000,
});

try {
// Your code goes here
} catch (err) {
console.error("Running the task on Golem failed due to", err);
} finally {
await executor.end();
}
})();
23 changes: 21 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 1 addition & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,6 @@
"marketplace",
"cli"
],
"files": [
"dist"
],
"author": "GolemFactory <[email protected]>",
"license": "GPL-3.0",
"engines": {
Expand All @@ -46,6 +43,7 @@
"ajv": "^8.12.0",
"ajv-formats": "^2.1.1",
"commander": "^11.0.0",
"enquirer": "^2.4.1",
"lodash": "^4.17.21",
"luxon": "^3.4.3",
"new-find-package-json": "^2.0.0",
Expand Down
3 changes: 2 additions & 1 deletion src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import { Command } from "commander";
import { version } from "./lib/version";
import { manifestCommand } from "./manifest/manifest.command";
import { newCommand } from "./new/new.command";

const program = new Command("golem-sdk");
program.version(version);
Expand All @@ -10,6 +11,6 @@ program.version(version);
// chalk.level = 0;
// });

program.addCommand(manifestCommand);
program.addCommand(manifestCommand).addCommand(newCommand);

program.parse();
171 changes: 171 additions & 0 deletions src/new/new.action.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
import { NewOptions, newProjectNameError, newProjectNameRegEx } from "./new.options";

import { prompt } from "enquirer";
import { join } from "path";
import { existsSync } from "fs";
import { cp, readFile, writeFile } from "fs/promises";

async function getName(providedName: string): Promise<string> {
if (!providedName) {
const result = (await prompt({
type: "input",
name: "name",
message: "Project name",
validate(value: string) {
return !newProjectNameRegEx.test(value) ? newProjectNameError : true;
},
})) as { name: string };

providedName = result.name;
} else {
if (!newProjectNameRegEx.test(providedName)) {
console.error(`Error: Project name ${providedName} is invalid: ${newProjectNameError}`);
process.exit(1);
}
}

return providedName;
}

async function getVersion(providedVersion?: string): Promise<string> {
if (typeof providedVersion === "string") {
return providedVersion;
}

const result = (await prompt({
type: "input",
name: "version",
message: "Project version",
initial: "1.0.0",
})) as { version: string };

return result.version;
}

async function getTemplate(providedTemplate?: string): Promise<string> {
if (typeof providedTemplate !== "string") {
const result = (await prompt({
type: "select",
name: "template",
message: "Select a project template",
choices: [
{ name: "js-node", hint: "Plain Javascript CLI application" },
// { name: "js-webapp", hint: "Plain Javascript Express based web application" },
],
})) as { template: string };

providedTemplate = result.template;
}

if (!newProjectNameRegEx.test(providedTemplate)) {
console.error(`Error: Template name ${providedTemplate} is invalid.`);
process.exit(1);
}

return providedTemplate;
}

async function getDescription(providedDescription?: string): Promise<string> {
if (typeof providedDescription === "string") {
return providedDescription;
}

const result = (await prompt({
type: "input",
name: "description",
message: "Project description",
initial: "An unique and awesome application that runs on Golem Network",
})) as { description: string };

return result.description;
}

type PackageJsonBasic = {
name: string;
description: string;
version: string;
author?: string;
};

async function updatePackageJson(projectPath: string, data: PackageJsonBasic): Promise<void> {
const packageJson = join(projectPath, "package.json");
let input: string;
let json: PackageJsonBasic;

try {
input = await readFile(packageJson, "utf8");
} catch (e) {
console.error(`Error: Failed to read ${packageJson}: ${e}`);
process.exit(1);
}

try {
json = JSON.parse(input);
} catch (e) {
console.error(`Error: Failed to parse ${packageJson}: ${e}`);
process.exit(1);
}

json.name = data.name;
json.description = data.description;
json.version = data.version;
if (data.author) {
json.author = data.author;
}

try {
await writeFile(packageJson, JSON.stringify(json, null, 2));
} catch (e) {
console.error(`Error: Failed to write ${packageJson}: ${e}`);
process.exit(1);
}
}

export async function newAction(providedName: string, options: NewOptions) {
const name = await getName(providedName);
const projectPath = options.path ?? join(process.cwd(), name);
if (existsSync(projectPath)) {
console.error(`Error: ${projectPath} already exists.`);
process.exit(1);
}

const template = await getTemplate(options.template);
const templatePath = join(__dirname, "../../data/project-templates", template);
if (!existsSync(templatePath)) {
console.error(`Error: Template ${template} not found.`);
process.exit(1);
}

const description = await getDescription(options.description);
const version = await getVersion(options.version);
const author = options.author;

console.log(`Creating a new Golem app in ${projectPath}.`);

try {
await cp(templatePath, projectPath, { recursive: true });
} catch (e) {
console.error(`Error: Failed to copy template files: ${e}`);
process.exit(1);
}

// Update package.json
await updatePackageJson(projectPath, {
name,
description,
version,
author,
});

// TODO: Consider running npm install (or yarn, or whatever).

console.log(`Project created successfully in ${projectPath}.`);

if (!process.env.YAGNA_APPKEY) {
console.log(
"NOTE: You do not seem to have YAGNA_APPKEY environment variable defined. You will need to define it or provide a .env file with it to run your new appplication.",
);
}

// TODO: Show some next steps, or pull it from template directory.
}
20 changes: 20 additions & 0 deletions src/new/new.command.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Command } from "commander";
import { NewOptions } from "./new.options";

export const newCommand = new Command("new");

newCommand
.summary("Create new Golem project.")
.description("Create a new Golem project from template.")
.option("-p, --path <path>", "Path of the new project.")
.option("-t, --template <template>", "Template to be used for the project.")
.option("-d, --description <text>", "Description of the project.")
.option("-a, --author <name>", "Author of the project.") // TODO: try to read it from git config?
.option("-v, --version <version>", "Version of the project.")
// TODO: implement list-templates?
// .option("-l, --list-templates", "List available projecttemplates.")
.argument("[name]", "Name of the project.")
.action(async (name: string, options: NewOptions) => {
const action = await import("./new.action.js");
await action.default.newAction(name, options);
});
11 changes: 11 additions & 0 deletions src/new/new.options.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export interface NewOptions {
author?: string;
description?: string;
path?: string;
template?: string;
version?: string;
}

export const newProjectNameRegEx = /^[a-z0-9-_]+$/;
export const newProjectNameError =
"Project name may only contain lower case letters, numbers, hyphens and underscores.";

0 comments on commit 3d2d0e0

Please sign in to comment.