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
12 changes: 10 additions & 2 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 @@ -153,7 +153,15 @@ export async function buildApp(src: string, dest: string, network: string = "mai
if (new_build_files.includes(file))
continue;

await remove(file);
const filePathArr = file.split(path.sep);
do {
const dir = filePathArr.join(path.sep);
const files = await readdir(dir).catch(() => ([]));
if (files.length == 0)
await remove(dir);

filePathArr.pop();
} while (filePathArr.length > 3)
}

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("[string]", "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
100 changes: 98 additions & 2 deletions lib/deploy.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,105 @@
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 } 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 translating = log.loading(`[${dist}] Translating files from src/widget to src`, LogLevels.BUILD);

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 }).catch(() => {
translating.error(`Failed to translate: ${path.join(srcDir, file)}`);
});
}

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

await remove(path.join(targetDir, file)).catch(() => {
translating.error(`Failed to remove: ${path.join(targetDir, file)}`);
});
}

translating.finish(`[${dist}] Translated successfully`);
}

// 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 deploying = log.loading(`[${src}] Deploying app`, LogLevels.BUILD);

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

await translateForBosCli(dist);

// Deploy using 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;

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(`[${src}] App deployed successfully`);
return;
}

deploying.error(error.message);
}
);
deploying.finish(`[${src}] App deployed successfully`);
}

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

export async function deploy(appName: string, opts: DeployOptions) {
const src = '.';
if (!appName) {
const dist = path.join(src, DEPLOY_DIST_FOLDER);

await deployAppCode(src, dist, opts);

} else {
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`);

const dist = path.join(DEPLOY_DIST_FOLDER, appSrc);

await deployAppCode(appSrc, dist, 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 } 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 };
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
11 changes: 5 additions & 6 deletions tests/unit/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,9 @@ const app_example_1_output = {
"/build/ipfs.json": JSON.stringify({
"logo.svg": "QmHash",
}, null, 2) + "\n",
"/build/src/widget/hello.utils.module.js": "const hello = (name) => `Hello, ${name}!`;\nreturn { hello };\n",
"/build/src/widget/hello/utils.module.js": "const hello = (name) => `Hello, ${name}!`;\nreturn { hello };\n",
Copy link
Contributor

Choose a reason for hiding this comment

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

It should be a dot, not /

"/build/src/widget/index.jsx": "const hello = \"hi\";\nreturn hello(props);\n",
"/build/src/widget/nested.index.jsx": "const hello = \"hi\";\nreturn hello(props);\n",
"/build/src/widget/nested/index.jsx": "const hello = \"hi\";\nreturn hello(props);\n",
Copy link
Contributor

Choose a reason for hiding this comment

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

Here too

"/build/src/widget/module.jsx": "VM.require(\"test.near/widget/hello.utils.module\");\nreturn hello(\"world\");\n",
"/build/src/widget/config.jsx": "return <h1>test.neartest.near</h1>;\n",
"/build/src/widget/alias.jsx": "return <h1>Hello world!</h1>;\n",
Expand Down Expand Up @@ -118,7 +118,7 @@ const app_example_2_output = {
"/build/ipfs.json": JSON.stringify({
"logo.svg": "QmHash",
}, null, 2) + "\n",
"/build/src/widget/hello.utils.module.js": "const hello = (name) => `Hello, ${name}!`;\nreturn { hello };\n",
"/build/src/widget/hello/utils.module.js": "const hello = (name) => `Hello, ${name}!`;\nreturn { hello };\n",
"/build/src/widget/index.jsx": "const hello = \"hi\";\nreturn hello(props);\n",
Copy link
Contributor

Choose a reason for hiding this comment

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

Here

"/build/src/widget/module.jsx": "VM.require(\"test.near/widget/hello.utils.module\");\nreturn hello(\"world\");\n",
"/build/src/widget/config.jsx": "return <h1>test.neartest.near</h1>;\n",
Expand Down 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 build correctly', async () => {
await deployAppCode('/app_example', '/build', {});
expect(vol.toJSON('/build')).toEqual(app_example_output);
})
})
Loading