-
Notifications
You must be signed in to change notification settings - Fork 24
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
Feat/deploy #82
Changes from 6 commits
f76e124
3d1d660
7882619
3aa6edd
a57975d
8ff1cdf
8da4211
1cbb611
1f7febc
5b1b749
1705cfe
6f69cb7
9b99ff8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,9 +1,102 @@ | ||
import { BaseConfig } from "./config"; | ||
import path from "path"; | ||
import { exec, ExecException } from "child_process"; | ||
import { BaseConfig, readConfig } from "./config"; | ||
import { buildApp } from "./build"; | ||
import { readWorkspace } from "./workspace"; | ||
import { Network } from "./types"; | ||
import { readdir, rename, remove } from "@/lib/utils/fs"; | ||
|
||
const DEPLOY_DIST_FOLDER = "build"; | ||
|
||
export type DeployOptions = { | ||
deployAccountId: string; | ||
signerAccountId: string; | ||
signerPublicKey: string; | ||
signerPrivateKey: string; | ||
network?: Network; | ||
}; | ||
|
||
// 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); | ||
|
||
const config = await readConfig(path.join(src, "bos.config.json"), opts.network); | ||
|
||
// Move files from "src/widget" to "src/" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looks great, how about moving L25-53 to a "translate" or "translateForBosCli" function so we can test it? |
||
const srcDir = path.join(dist, "src", "widget"); | ||
const targetDir = path.join(dist, "src"); | ||
|
||
const original_files = await readdir(targetDir).catch(() => ([])); | ||
for (const file of original_files) { | ||
if (file == "widget") | ||
continue; | ||
|
||
await remove(file).catch(() => { | ||
deploying.error(`Failed to remove ${path.join(targetDir, file)}`); | ||
}) | ||
} | ||
|
||
const new_files = await readdir(srcDir).catch(() => ([])); | ||
if (new_files.length === 0) { | ||
deploying.error(`Failed to read ${srcDir}`); | ||
return; | ||
} | ||
|
||
for (const file of new_files) { | ||
rename(path.join(srcDir, file), path.join(targetDir, file)).catch(() => { | ||
deploying.error(`Failed to move ${path.join(srcDir, file)}`); | ||
}); | ||
} | ||
|
||
remove(path.join(targetDir, "widget")).catch(() => { | ||
deploying.error(`Failed to remove widget`); | ||
}); | ||
|
||
// Deploy using bos-cli; | ||
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; | ||
|
||
await 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); | ||
} | ||
); | ||
} | ||
|
||
// 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 buildApp(src, dist, opts.network); | ||
|
||
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 buildApp(appSrc, dist, opts.network); | ||
|
||
deployAppCode(appSrc, dist, opts); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,113 @@ | ||
import { deploy } 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); | ||
|
||
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('build', () => { | ||
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 without logs', async () => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I like how you've set up these tests so far -- for test cases, I'm thinking:
|
||
const { logs } = await deploy('/app_example', {}); | ||
expect(logs).toEqual([]); | ||
expect(vol.toJSON('/build')).toEqual(app_example_output); | ||
elliotBraem marked this conversation as resolved.
Show resolved
Hide resolved
|
||
}) | ||
}) |
There was a problem hiding this comment.
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?