Added nodejs code generator. (#1)

This commit is contained in:
Ravin Perera
2022-06-18 23:23:31 +05:30
committed by GitHub
parent 87354cad69
commit f5c366fbd6
8 changed files with 260 additions and 40 deletions

View File

@@ -64,6 +64,12 @@ Example `hp.cfg.override` for a nodejs application:
}
```
#### Code generator
```
# Generate nodejs starter project
.\hpdevkit.ps1 gen nodejs starter <project name>
```
### Generate executable
```powershell
cd windows

View File

@@ -22,4 +22,7 @@ RUN chmod -R +x /build/scripts/*.sh
FROM ubuntu:focal as runner
COPY --from=builder /build/docker-extracted/usr/bin/docker /usr/bin/
COPY --from=builder /build/scripts/cluster.sh /usr/bin/cluster
COPY --from=builder /build/jq /usr/bin/jq
COPY --from=builder /build/scripts/codegen.sh /usr/bin/codegen
COPY --from=builder /build/jq /usr/bin/jq
COPY code-templates /code-templates

View File

@@ -0,0 +1,6 @@
{
"contract": {
"bin_path": "/usr/bin/node",
"bin_args": "index.js"
}
}

View File

@@ -0,0 +1,13 @@
{
"name": "_projname_",
"version": "1.0.0",
"scripts": {
"build": "npx ncc build src/contract.js -o dist",
"build:prod": "npx ncc build src/contract.js --minify -o dist",
"start": "npm run build && hpdevkit deploy dist"
},
"dependencies": {
"hotpocket-nodejs-contract": "0.5.3",
"@vercel/ncc": "0.34.0"
}
}

View File

@@ -0,0 +1,60 @@
const fs = require('fs').promises;
// This sample application writes and reads from a simple text file to serve user requests.
// Real-world applications may use a proper local database like sqlite.
const dataFile = 'datafile.txt'
export class _projname_ {
sendOutput; // This function must be wired up by the caller.
async handleRequest(userPubKey, message, isReadOnly) {
// This sample application defines two simple messages. 'get' and 'set'.
// It's up to the application to decide the structure and contents of messages.
if (message.type == 'get') {
// Retrieved previously saved data and return to the user.
const data = await this.getData();
await this.sendOutput(userPubKey, {
type: 'data_result',
data: data
})
}
else if (message.type == 'set') {
if (!isReadOnly) {
// Save the provided data into storage.
await this.setData(message.data);
}
else {
await this.sendOutput(userPubKey, {
type: 'error',
error: 'Set data not supported in readonly mode'
})
}
}
else {
await this.sendOutput(userPubKey, {
type: 'error',
error: 'Unknown message type'
})
}
}
async setData(data) {
// Hot Pocket subjects data on-disk to consensus.
await fs.writeFile(dataFile, data);
}
async getData() {
try {
return (await fs.readFile(dataFile)).toString();
}
catch {
console.log('Data file not created yet. Returning empty data.');
return '';
}
}
}

View File

@@ -0,0 +1,49 @@
const HotPocket = require('hotpocket-nodejs-contract');
const { _projname_ } = require('./_projname_');
// Hot Pocket smart contract is defined as a function which takes the Hot Pocket ExecutionContext as an argument.
// This function gets invoked every consensus round and whenever a user sends a out-of-concensus read-request.
async function contract(ctx) {
// Create our application logic component.
// This pattern allows us to test the application logic independently of Hot Pocket.
const app = new _projname_();
// Wire-up output emissions from the application before we pass user inputs to it.
app.sendOutput = async (userPubKey, output) => {
// In Hot Pocket, each user is represented by their Ed25519 public key.
const user = ctx.users.find(userPubKey);
await user.send(output)
}
// In 'readonly' mode, nothing our contract does will get persisted on the ledger. The benefit is
// readonly messages gets processed much faster due to not being subjected to consensus.
// We should only use readonly mode for returning/replying data for the requesting user.
//
// In consensus mode (NOT read-only), we can do anything like persisting to data storage and/or
// sending data to any connected user at the time. Everything will get subjected to consensus so
// there is a time-penalty.
const isReadOnly = ctx.readonly;
// Process user inputs.
// Loop through list of users who have sent us inputs.
for (const user of ctx.users.list()) {
// Loop through inputs sent by each user.
for (const input of user.inputs) {
// Read the data buffer sent by user (this can be any kind of data like string, json or binary data).
const buf = await ctx.users.read(input);
// Let's assume all data buffers for this contract are JSON.
// In real-world apps, we need to gracefully fitler out invalid data formats for our contract.
const message = JSON.parse(buf);
// Pass the JSON message to our application logic component.
await app.handleRequest(userPubKey, message, isReadOnly);
}
}
}
const hpc = new HotPocket.Contract();
hpc.init(contract);

40
docker/scripts/codegen.sh Normal file
View File

@@ -0,0 +1,40 @@
#!/bin/bash
platform=$1
apptype=$2
projname=$3
templates_dir="/code-templates"
placeholder="_projname_"
output_dir=$CODEGEN_OUTPUT
usage="Usage: <platform> <app type> <project name>"
if [ -z $templates_dir ] || [ -z $output_dir ] ; then
echo "Mandatory values missing." && exit 1
fi
[ -z $platform ] && echo "Platform is required. $usage" && exit 1
[ -z $apptype ] && echo "App type is required. $usage" && exit 1
[ -z $projname ] && echo "Project name is required. $usage" && exit 1
[ ! -d $templates_dir/$platform ] && echo "Invalid platform '$platform' specified." && exit 1
[ ! -d $templates_dir/$platform/$apptype ] && echo "Invalid application type '$apptype' specified." && exit 1
if ! [[ "$projname" =~ ^[a-z][a-z0-9_-]*$ ]]; then
echo "Invalid project name. Must be lowercase. Must start with a letter. Can only contain letters, numbers, dash and underscore."
exit 1
fi
mkdir -p $output_dir && rm -rf $output_dir/*
cp -r $templates_dir/$platform/$apptype/* $output_dir
pushd $output_dir > /dev/null
# Replace placeholder in all file contents.
find -type f -exec sed -i "s/${placeholder}/${projname}/g" {} \;
# Rename files with placeholder name.
for f in $(find -type f -name "*${placeholder}*")
do
mv "$f" "$(echo "$f" | sed s/${placeholder}/${projname}/g)"
done
popd > /dev/null

View File

@@ -12,9 +12,12 @@ $Network = "$($GlobalPrefix)_$($Cluster)_net"
$ContainerPrefix = "$($GlobalPrefix)_$($Cluster)_node"
$BundleMount = "$($VolumeMount)/contract_bundle"
$DeploymentContainerName = "$($GlobalPrefix)_$($Cluster)_deploymgr"
$CodegenContainerName = "$($GlobalPrefix)_codegen"
$ConfigOverridesFile = "hp.cfg.override"
$CodegenOutputDir = "/codegen-output"
$DefaultCodegenProject = "hpdevkitproject"
function DevKitContainer([string]$Mode, [string]$Name, [switch]$Detached, [switch]$AutoRemove, [switch]$MountSock, [switch]$MountVolume, [string]$Cmd) {
function DevKitContainer([string]$Mode, [string]$Name, [switch]$Detached, [switch]$AutoRemove, [switch]$MountSock, [switch]$MountVolume, [string]$EntryPoint, [string]$Cmd, [switch]$Status) {
$Command = "docker $($Mode) -it"
if ($Name) {
@@ -35,17 +38,38 @@ function DevKitContainer([string]$Mode, [string]$Name, [switch]$Detached, [switc
$Command += " --mount type=volume,src=$($Volume),dst=$($VolumeMount)"
}
if ($EntryPoint) {
$Command += " --entrypoint $($EntryPoint)"
}
else {
$Command += " --entrypoint /bin/bash"
}
# Pass environment variables used by our scripts.
$Command += " -e CLUSTER=$($Cluster) -e CLUSTER_SIZE=$($ClusterSize) -e DEFAULT_NODE=$($DefaultNode) -e VOLUME=$($Volume) -e NETWORK=$($Network)"
$Command += " -e CONTAINER_PREFIX=$($ContainerPrefix) -e VOLUME_MOUNT=$($VolumeMount) -e BUNDLE_MOUNT=$($BundleMount) -e HOTPOCKET_IMAGE=$($InstanceImage)"
$Command += " -e CONFIG_OVERRIDES_FILE=$($ConfigOverridesFile)"
$Command += " -e CONFIG_OVERRIDES_FILE=$($ConfigOverridesFile) -e CODEGEN_OUTPUT=$($CodegenOutputDir)"
$Command += " $($DevKitImage)"
if ($Cmd) {
$Command += " /bin/bash -c '$($Cmd)'"
if ($EntryPoint) {
$Command += " $($Cmd)"
}
else {
$Command += " -c '$($Cmd)'"
}
}
Invoke-Expression $Command
Invoke-Expression $Command 2>&1 | Write-Host
if ($Status) {
if ($LASTEXITCODE -eq 0) {
return $True
}
else {
return $False
}
}
}
function ExecuteInDeploymentContainer([string]$Cmd) {
@@ -76,53 +100,72 @@ function TeardownDeploymentCluster() {
Function Deploy([string]$Path) {
InitializeDeploymentCluster
if ($Path) {
InitializeDeploymentCluster
# If copying a directory, delete target bundle directory. If not create empty target bundle directory to copy a file.
$PrepareBundleDir = ""
if ((Get-Item $Path) -is [System.IO.DirectoryInfo]) {
$PrepareBundleDir = "rm -rf $($BundleMount)"
# If copying a directory, delete target bundle directory. If not create empty target bundle directory to copy a file.
$PrepareBundleDir = ""
if ((Get-Item $Path) -is [System.IO.DirectoryInfo]) {
$PrepareBundleDir = "rm -rf $($BundleMount)"
}
else {
$PrepareBundleDir = "mkdir -p $($BundleMount) && rm -rf $($BundleMount)/* $($BundleMount)/.??*"
}
ExecuteInDeploymentContainer -Cmd $PrepareBundleDir
docker cp $Path "$($DeploymentContainerName):$($BundleMount)"
# Sync contract bundle to all instance directories in the cluster.
ExecuteInDeploymentContainer -Cmd "cluster stop ; cluster sync ; cluster start"
if ($DefaultNode -gt 0) {
Write-Host "Streaming logs of node $($DefaultNode):"
ExecuteInDeploymentContainer -Cmd "cluster logs $($DefaultNode)"
}
}
else {
$PrepareBundleDir = "mkdir -p $($BundleMount) && rm -rf $($BundleMount)/* $($BundleMount)/.??*"
}
ExecuteInDeploymentContainer -Cmd $PrepareBundleDir
docker cp $Path "$($DeploymentContainerName):$($BundleMount)"
# Sync contract bundle to all instance directories in the cluster.
ExecuteInDeploymentContainer -Cmd "cluster stop ; cluster sync ; cluster start"
if ($DefaultNode -gt 0) {
Write-Host "Streaming logs of node $($DefaultNode):"
ExecuteInDeploymentContainer -Cmd "cluster logs $($DefaultNode)"
Write-Host "Please specify directory or file path to deploy."
}
}
Write-Host "Hot Pocket devkit launcher"
Function CodeGenerator() {
$ProjName = $args[2]
if (Test-Path -Path $ProjName) {
"Directory '$($ProjName)' already exists."
return
}
if (DevKitContainer -Status -Mode "run" -Name $CodegenContainerName -EntryPoint "codegen" -Cmd "$($args[0]) $($args[1]) $($ProjName)") {
docker cp "$($CodegenContainerName):$($CodegenOutputDir)" ./$ProjName
Write-Host "Project '$($ProjName)' created."
}
docker rm "$($CodegenContainerName)" 2>&1 | Out-Null
}
$Command = $args[0]
$CommandError = "Invalid command. Expected: deploy | clean | start | stop | logs"
$CommandError = "Invalid command. Expected: deploy | clean | start | stop | logs | gen"
if ($Command) {
Write-Host "command: $($Command) (cluster: $($Cluster))"
if ($Command -eq "deploy") {
$Path = $args[1]
if ($Path) {
Deploy -Path $Path
}
else {
Write-Host "Please specify directory or file path to deploy."
}
}
elseif ($Command -eq "clean") {
TeardownDeploymentCluster
}
elseif ($Command -eq "logs" -OR $Command -eq "start" -OR $Command -eq "stop") {
DevKitContainer -Mode "run" -AutoRemove -MountSock -Cmd "cluster $($args)"
if ($Command -eq "gen") {
Write-Host "Hot Pocket devkit code generator"
CodeGenerator $args[1] $args[2] $args[3]
}
else {
Write-Host $CommandError
Write-Host "Hot Pocket devkit launcher"
Write-Host "command: $($Command) (cluster: $($Cluster))"
if ($Command -eq "deploy") {
Deploy -Path $args[1]
}
elseif ($Command -eq "clean") {
TeardownDeploymentCluster
}
elseif ($Command -eq "logs" -OR $Command -eq "start" -OR $Command -eq "stop") {
DevKitContainer -Mode "run" -AutoRemove -MountSock -EntryPoint "cluster" -Cmd "$($args)"
}
else {
Write-Host $CommandError
}
}
}
else {