Skip to content

Commit

Permalink
Implement deploy command (#82)
Browse files Browse the repository at this point in the history
  • Loading branch information
ittechhunter authored Apr 29, 2024
1 parent a8b11f5 commit 214361a
Show file tree
Hide file tree
Showing 8 changed files with 266 additions and 21 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ Commands:
init [options] [path] Initialize a new project
clone [account] [dest] Clone a SocialDB repository
pull [account] Pull updates from a SocialDB repository
deploy [string] Deploy the project (not implemented)
deploy [options] [appName] Deploy the project
upload [string] Upload data to SocialDB (not implemented)
help [command] display help for command
```
Expand Down
16 changes: 7 additions & 9 deletions lib/build.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import path from "path";
import { readConfig } from "@/lib/config";
import { writeJson, copy, loopThroughFiles, outputFile, readFile, readJson, remove } from "@/lib/utils/fs";
import { writeJson, copy, loopThroughFiles, outputFile, readdir, readFile, readJson, remove } from "@/lib/utils/fs";
import { transpileJS, EvalCustomSyntaxParams } from "@/lib/parser";
import { Log } from "@/lib/types";
import { UploadToIPFSOptions, uploadToIPFS } from "@/lib/ipfs";
Expand Down Expand Up @@ -67,7 +67,6 @@ export async function buildApp(src: string, dest: string, network: string = "mai
};

const new_build_files: string[] = [];
const original_build_files: string[] = [];

// module transpilation
const loadingModules = log.loading(`Transpiling ${modules.length} modules`, LogLevels.BUILD);
Expand All @@ -90,7 +89,7 @@ export async function buildApp(src: string, dest: string, network: string = "mai
logs.push(...new_logs);

// write to dest
let new_file_name = path.relative(path.join(src, "module"), file).replace("/", ".");
let new_file_name = path.relative(path.join(src, "module"), file).replace(path.sep, ".");
new_file_name = new_file_name.substring(0, new_file_name.length - path.extname(file).length);
new_file_name += ".module.js";

Expand Down Expand Up @@ -129,7 +128,7 @@ export async function buildApp(src: string, dest: string, network: string = "mai
logs.push(...new_logs);

// write to dest
let new_file_name = path.relative(path.join(src, "widget"), file).replace("/", ".");
let new_file_name = path.relative(path.join(src, "widget"), file).replace(path.sep, ".");
new_file_name = new_file_name.substring(0, new_file_name.length - path.extname(file).length);
new_file_name += ".jsx";

Expand All @@ -146,14 +145,13 @@ export async function buildApp(src: string, dest: string, network: string = "mai
}

// remove unnecessary build files
await loopThroughFiles(path.join(dest, "src", "widget"), async (file: string) => {
original_build_files.push(file);
})
const original_build_files = await readdir(path.join(dest, "src", "widget")).catch(() => []);
for (const file of original_build_files) {

This comment has been minimized.

Copy link
@elliotBraem

elliotBraem Apr 30, 2024

Contributor

How come this was replaced?

A change in this file introduced a bug where deeply nested widget paths don't work anymore. See bug

I've started a branch with a good test that is currently failing, make the fix on this branch: #89

if (new_build_files.includes(file))
const filePath = path.join(dest, "src", "widget", file);
if (new_build_files.includes(filePath))
continue;

await remove(file);
await remove(filePath);
}

await log.wait(
Expand Down
19 changes: 15 additions & 4 deletions lib/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import path from "path";
import { dev } from "./dev";
import { cloneRepository } from "./repository";
import { buildWorkspace, devWorkspace } from "./workspace";
import { deploy } from "./deploy";

const program = new Command();

Expand Down Expand Up @@ -122,11 +123,21 @@ async function run() {

program
.command("deploy")
.description("Deploy the project (not implemented)")
.argument("[string]", "app name")
.action((appName) => {
console.log("not yet supported");
.description("Deploy the project")
.argument("[appName]", "Workspace app name to deploy")
.option("--deploy-account-id <deployAccountId>", "Account under which component code should be deployed")
.option("--signer-account-id <signerAccountId>", "Account which will be used for signing deploy transaction, frequently the same as deploy-account-id")
.option("--signer-public-key <signerPublicKey>", "Public key for signing transactions in the format: `ed25519:<public_key>`")
.option("--signer-private-key <signerPrivateKey>", "Private key in `ed25519:<private_key>` format for signing transaction")
.option("-n, --network <network>", "network to deploy for", "mainnet")
.option("-l, --loglevel <loglevel>", "log level (ERROR, WARN, INFO, DEV, BUILD, DEBUG)")
.action(async (appName, opts) => {
global.log = new Logger(LogLevel?.[opts?.loglevel?.toUpperCase() as keyof typeof LogLevel] || LogLevel.BUILD);
await deploy(appName, opts).catch((e: Error) => {
log.error(e.stack || e.message);
})
});

program
.command("upload")
.description("Upload data to SocialDB (not implemented)")
Expand Down
122 changes: 120 additions & 2 deletions lib/deploy.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,127 @@
import { BaseConfig } from "./config";
import path from "path";
import { exec, ExecException } from "child_process";

import { BaseConfig, readConfig } from "@/lib/config";
import { buildApp } from "@/lib/build";
import { readWorkspace } from "@/lib/workspace";
import { Log, Network } from "@/lib/types";
import { readdir, remove, move, pathExists } from "@/lib/utils/fs";
import { Logger } from "./logger";

const DEPLOY_DIST_FOLDER = "build";

export type DeployOptions = {
deployAccountId?: string;
signerAccountId?: string;
signerPublicKey?: string;
signerPrivateKey?: string;
network?: Network;
};

// translate files from src/widget to src
export async function translateForBosCli(dist: string) {
const srcDir = path.join(dist, "src", "widget");
const targetDir = path.join(dist, "src");

const new_files = await readdir(srcDir).catch(() => ([]));
const original_files = await readdir(targetDir).catch(() => ([]));

for (const file of new_files) {
await move(path.join(srcDir, file), path.join(targetDir, file), { overwrite: true });
}

for (const file of original_files) {
if (new_files.includes(file))
continue;

await remove(path.join(targetDir, file));
}
}

// deploy the app widgets and modules
export async function deployAppCode(src: string, config: BaseConfig) {
export async function deployAppCode(src: string, dist: string, opts: DeployOptions) {
const fullSrc = path.resolve(src);
const fullDist = path.resolve(dist);

const deploying = log.loading(`[${fullSrc}] Deploying app`, LogLevels.BUILD);

// Build
await buildApp(src, dist, opts.network);

// Translate for bos cli
await log.wait(
translateForBosCli(dist),
`[${fullDist}] Translating files for bos cli`,
`[${fullDist}] Translated successfully`,
`[${fullDist}] Failed to translate`,
LogLevels.BUILD
);

// Exec bos-cli;
const config = await readConfig(path.join(src, "bos.config.json"), opts.network);

const BOS_DEPLOY_ACCOUNT_ID = config.accounts.deploy || opts.deployAccountId;
const BOS_SIGNER_ACCOUNT_ID = config.accounts.signer || opts.signerAccountId;
const BOS_SIGNER_PUBLIC_KEY = opts.signerPublicKey;
const BOS_SIGNER_PRIVATE_KEY = opts.signerPrivateKey;

if (!BOS_DEPLOY_ACCOUNT_ID) {
deploying.error(`Necessary values not provided, please provide Account ID for deploy`);
return;
} else if (!BOS_SIGNER_ACCOUNT_ID) {
deploying.error(`Necessary values not provided, please provide Signer Account ID for deploy`);
return;
} else if (!BOS_SIGNER_PUBLIC_KEY || !BOS_SIGNER_PRIVATE_KEY) {
deploying.error(`Necessary values not provided, please provide private & public key for deploy`);
return;
}

exec(
`cd ${dist} && npx bos components deploy "${BOS_DEPLOY_ACCOUNT_ID}" sign-as "${BOS_SIGNER_ACCOUNT_ID}" network-config "${opts.network}" sign-with-plaintext-private-key --signer-public-key "${BOS_SIGNER_PUBLIC_KEY}" --signer-private-key "${BOS_SIGNER_PRIVATE_KEY}" send`,
(error: ExecException | null, stdout: string, stderr: string) => {
if (!error) {
deploying.finish(`[${fullSrc}] App deployed successfully`);
return;
}

deploying.error(error.message);
}
);
}

// publish data.json to SocialDB
export async function deployAppData(src: string, config: BaseConfig) {
}

export async function deploy(appName: string, opts: DeployOptions) {
const src = '.';

// Deploy single project
if (!appName) {
if (await pathExists(path.join(src, "bos.config.json"))) { // Check if the directory has bos.config.json file
await deployAppCode(src, path.join(src, DEPLOY_DIST_FOLDER), opts);
return;
} else { // Check if the directory has bos.workspace.json file
if (await pathExists(path.join(src, "bos.workspace.json"))) {
log.error(`Please provide app name`);
return;
}
}

log.error(`[${src}] bos.config.json file is not existing in the project`);
return;
}

// Deploy workspace app
const { apps } = await readWorkspace(src);

const findingApp = log.loading(`Finding ${appName} in the workspace`, LogLevels.BUILD);
const appSrc = apps.find((app) => app.includes(appName));
if (!appSrc) {
findingApp.error(`Not found ${appName} in the workspace`);
return;
}
findingApp.finish(`Found ${appName} in the workspace`);

await deployAppCode(appSrc, path.join(DEPLOY_DIST_FOLDER, appSrc), opts);
}
4 changes: 2 additions & 2 deletions lib/utils/fs.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { copy, readFile, lstat, readJson, writeJson, ensureDir, outputFile, readdir, remove } from 'fs-extra';
import { copy, readFile, lstat, readJson, writeJson, ensureDir, outputFile, readdir, remove, move, pathExists } from 'fs-extra';
import path from 'path';

async function loopThroughFiles(pwd: string, callback: (file: string) => Promise<void>) {
Expand All @@ -16,4 +16,4 @@ async function loopThroughFiles(pwd: string, callback: (file: string) => Promise
}
}

export { copy, readJson, writeJson, ensureDir, outputFile, loopThroughFiles, readFile, remove };
export { copy, readJson, writeJson, ensureDir, outputFile, loopThroughFiles, readFile, readdir, remove, move, pathExists };
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"author": "",
"license": "ISC",
"dependencies": {
"bos-cli": "^0.3.13",
"commander": "^11.1.0",
"crypto-js": "^4.2.0",
"express": "^4.18.2",
Expand Down
5 changes: 2 additions & 3 deletions tests/unit/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,9 +168,8 @@ describe('build', () => {
global.log = unmockedLog;
})

it('should build correctly without logs', async () => {
const { logs } = await buildApp('/app_example_1', '/build');
expect(logs).toEqual([]);
it('should build correctly', async () => {
await buildApp('/app_example_1', '/build');
expect(vol.toJSON('/build')).toEqual(app_example_1_output);
})

Expand Down
118 changes: 118 additions & 0 deletions tests/unit/deploy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import * as process from "child_process";
import { deployAppCode } from '@/lib/deploy';
import { BaseConfig, DEFAULT_CONFIG } from '@/lib/config';
import * as fs from '@/lib/utils/fs';
import { LogLevel, Logger } from "@/lib/logger";

import { vol, } from 'memfs';
jest.mock('fs', () => require('memfs').fs);
jest.mock('fs/promises', () => require('memfs').fs.promises);
jest.mock('child_process', () => ({
exec: jest.fn((command: string) => {
return command;
}),
}))

const app_example = {
"./bos.config.json": JSON.stringify({
...DEFAULT_CONFIG,
account: "test.near",
ipfs: {
gateway: "https://testipfs/ipfs",
},
format: true,
}),
"./aliases.json": JSON.stringify({
"name": "world",
}),
"./ipfs/logo.svg": "<svg viewBox='0 0 100 100'><circle cx='50' cy='50' r='50' fill='red' /></svg>",
"./module/hello/utils.ts": "const hello = (name: string) => `Hello, ${name}!`; export default { hello };",
"./widget/index.tsx": "type Hello = {}; const hello: Hello = 'hi'; export default hello;",
"./widget/index.metadata.json": JSON.stringify({
name: "Hello",
description: "Hello world widget",
}),
"./widget/nested/index.tsx": "type Hello = {}; const hello: Hello = 'hi'; export default hello;",
"./widget/nested/index.metadata.json": JSON.stringify({
name: "Nested Hello",
description: "Nested Hello world widget",
}),
"./widget/module.tsx": "VM.require('${module_hello_utils}'); export default hello('world');",
"./widget/config.jsx": "return <h1>${config_account}${config_account_deploy}</h1>;",
"./widget/alias.tsx": "export default <h1>Hello ${alias_name}!</h1>;",
"./widget/ipfs.tsx": "export default <img height='100' src='${ipfs_logo.svg}' />;",
"./data/thing/data.json": JSON.stringify({
"type": "efiz.near/type/thing",
}),
"./data/thing/datastring.jsonc": JSON.stringify({
name: "Thing",
}),
};

const app_example_output = {
"/build/ipfs.json": JSON.stringify({
"logo.svg": "QmHash",
}, null, 2) + "\n",
"/build/src/hello.utils.module.js": "const hello = (name) => `Hello, ${name}!`;\nreturn { hello };\n",
"/build/src/index.jsx": "const hello = \"hi\";\nreturn hello(props);\n",
"/build/src/nested.index.jsx": "const hello = \"hi\";\nreturn hello(props);\n",
"/build/src/module.jsx": "VM.require(\"test.near/widget/hello.utils.module\");\nreturn hello(\"world\");\n",
"/build/src/config.jsx": "return <h1>test.neartest.near</h1>;\n",
"/build/src/alias.jsx": "return <h1>Hello world!</h1>;\n",
"/build/src/ipfs.jsx": "return <img height=\"100\" src=\"https://testipfs/ipfs/QmHash\" />;\n",
"/build/data.json": JSON.stringify({
"test.near": {
thing: {
data: {
"type": "efiz.near/type/thing",
},
datastring: JSON.stringify({
name: "Thing",
})
},
widget: {
index: {
metadata: {
name: "Hello",
description: "Hello world widget",
}
},
"nested.index": {
metadata: {
name: "Nested Hello",
description: "Nested Hello world widget",
}

}
}
}
}, null, 2) + "\n",
};

const unmockedFetch = global.fetch;
const unmockedLog = global.log;

describe('deploy', () => {
beforeEach(() => {
vol.reset();
vol.fromJSON(app_example, '/app_example');

global.fetch = (() => {
return Promise.resolve({
json: () => Promise.resolve({
cid: "QmHash",
})
})
}) as any;
global.log = new Logger(LogLevel.ERROR);
})
afterAll(() => {
global.fetch = unmockedFetch;
global.log = unmockedLog;
})

it('should match expected input for bos-cli-rs', async () => {
await deployAppCode('/app_example', '/build', {});
expect(vol.toJSON('/build')).toEqual(app_example_output);
})
})

0 comments on commit 214361a

Please sign in to comment.