Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/deploy #82

Merged
merged 13 commits into from
Apr 29, 2024
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) {
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")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This all looks good, can you run bw help and update the README with this new command?

.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);
elliotBraem marked this conversation as resolved.
Show resolved Hide resolved
})
})
Loading