Use go module (#303)

* Migrate to gomodule

* Fix install

* Update makefile
This commit is contained in:
Sung Won Cho 2019-11-11 15:28:47 +08:00 committed by GitHub
commit 2124e28a9f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 399 additions and 720 deletions

109
scripts/cli/build.sh Executable file
View file

@ -0,0 +1,109 @@
#!/usr/bin/env bash
#
# build.sh compiles dnote binary for target platforms. It is resonsible for creating
# distributable files that can be released by a human or a script.
#
# It can either cross-compile for different platforms using xgo, simply target a specific
# platform. Set GOOS and GOARCH environment variables to disable xgo and instead
# compile for a specific platform.
#
# use:
# ./scripts/build.sh 0.4.8
# GOOS=linux GOARCH=amd64 ./scripts/build.sh 0.4.8
set -ex
dir=$(dirname "${BASH_SOURCE[0]}")
version=$1
projectDir="$dir/../.."
basedir="$projectDir/pkg/cli"
outputDir="$projectDir/build/cli"
command_exists () {
command -v "$1" >/dev/null 2>&1;
}
if ! command_exists shasum; then
echo "please install shasum"
exit 1
fi
if [ $# -eq 0 ]; then
echo "no version specified."
exit 1
fi
if [[ $1 == v* ]]; then
echo "do not prefix version with v"
exit 1
fi
goVersion=1.12.x
get_binary_name() {
platform=$1
if [ "$platform" == "windows" ]; then
echo "dnote.exe"
else
echo "dnote"
fi
}
build() {
platform=$1
arch=$2
# native indicates if the compilation is to take place natively on the host platform
# if not true, use xgo with Docker to cross-compile
native=$3
# build binary
destDir="$outputDir/$platform-$arch"
ldflags="-X main.apiEndpoint=https://api.dnote.io -X main.versionTag=$version"
tags="fts5"
mkdir -p "$destDir"
if [ "$native" == true ]; then
GOOS="$platform" GOARCH="$arch" \
go build \
-ldflags "$ldflags" \
--tags "$tags" \
-o="$destDir/cli-$platform-$arch" \
"$basedir"
else
xgo \
-go "$goVersion" \
--targets="$platform/$arch" \
-ldflags "$ldflags" \
--tags "$tags" \
--dest="$destDir" \
"$basedir"
fi
binaryName=$(get_binary_name "$platform")
mv "$destDir/cli-${platform}-"* "$destDir/$binaryName"
# build tarball
tarballName="dnote_${version}_${platform}_${arch}.tar.gz"
tarballPath="$outputDir/$tarballName"
cp "$projectDir/licenses/GPLv3.txt" "$destDir"
cp "$basedir/README.md" "$destDir"
tar -C "$destDir" -zcvf "$tarballPath" "."
rm -rf "$destDir"
# calculate checksum
pushd "$outputDir"
shasum -a 256 "$tarballName" >> "$outputDir/dnote_${version}_checksums.txt"
popd
}
if [ -z "$GOOS" ] && [ -z "$GOARCH" ]; then
# fetch tool
go get -u github.com/karalabe/xgo
build linux amd64
build darwin amd64
build windows amd64
else
build "$GOOS" "$GOARCH" true
fi

11
scripts/cli/dev.sh Executable file
View file

@ -0,0 +1,11 @@
#!/usr/bin/env bash
# dev.sh builds a new binary and replaces the old one in the PATH with it
set -eux
dir=$(dirname "${BASH_SOURCE[0]}")
sudo rm -rf "$(which dnote)" "$GOPATH/bin/cli"
# change tags to darwin if on macos
go install -ldflags "-X main.apiEndpoint=http://127.0.0.1:5000" --tags "linux fts5" "$dir/../../pkg/cli"
sudo ln -s "$GOPATH/bin/cli" /usr/local/bin/dnote

5
scripts/cli/dump_schema.sh Executable file
View file

@ -0,0 +1,5 @@
#!/usr/bin/env bash
# dump_schema.sh dumps the current system's dnote schema
set -eux
sqlite3 ~/.dnote/dnote.db .schema

14
scripts/cli/test.sh Executable file
View file

@ -0,0 +1,14 @@
#!/usr/bin/env bash
# test.sh runs test files sequentially
# https://stackoverflow.com/questions/23715302/go-how-to-run-tests-for-multiple-packages
set -eux
dir=$(dirname "${BASH_SOURCE[0]}")
pushd "$dir/../../pkg/cli"
# clear tmp dir in case not properly torn down
rm -rf "./tmp"
go test -a ./... \
-p 1\
--tags "fts5"
popd

View file

@ -55,9 +55,10 @@ agpl="/* Copyright (C) 2019 Monomax Software Pty Ltd
* along with Dnote. If not, see <https://www.gnu.org/licenses/>.
*/"
pkgPath="$GOPATH/src/github.com/dnote/dnote/pkg"
serverPath="$GOPATH/src/github.com/dnote/dnote/pkg/server"
browserPath="$GOPATH/src/github.com/dnote/dnote/browser"
dir=$(dirname "${BASH_SOURCE[0]}")
pkgPath="$dir/pkg"
serverPath="$dir/pkg/server"
browserPath="$dir/browser"
gplFiles=$(find "$pkgPath" "$browserPath" -type f \( -name "*.go" -o -name "*.js" -o -name "*.ts" -o -name "*.tsx" -o -name "*.scss" -o -name "*.css" \) ! -path "**/vendor/*" ! -path "**/node_modules/*" ! -path "$serverPath/*")
@ -66,8 +67,8 @@ for file in $gplFiles; do
add_notice "$file" "$gpl"
done
webPath="$GOPATH"/src/github.com/dnote/dnote/web
jslibPath="$GOPATH/src/github.com/dnote/dnote/jslib/src"
webPath="$dir/web"
jslibPath="$dir/jslib/src"
agplFiles=$(find "$serverPath" "$webPath" "$jslibPath" -type f \( -name "*.go" -o -name "*.js" -o -name "*.ts" -o -name "*.tsx" -o -name "*.scss" -o -name "*.css" \) ! -path "**/vendor/*" ! -path "**/node_modules/*" ! -path "**/dist/*")
for file in $agplFiles; do

61
scripts/server/build.sh Executable file
View file

@ -0,0 +1,61 @@
#!/usr/bin/env bash
set -eux
dir=$(dirname "${BASH_SOURCE[0]}")
version=$1
projectDir="$dir/../.."
basedir="$projectDir/pkg/server"
outputDir="$projectDir/build/server"
command_exists () {
command -v "$1" >/dev/null 2>&1;
}
if ! command_exists shasum; then
echo "please install shasum"
exit 1
fi
if [ $# -eq 0 ]; then
echo "no version specified."
exit 1
fi
if [[ $1 == v* ]]; then
echo "do not prefix version with v"
exit 1
fi
build() {
platform=$1
arch=$2
destDir="$outputDir/$platform-$arch"
mkdir -p "$destDir"
# build binary
packr2
GOOS="$platform" \
GOARCH="$arch" go build \
-o "$destDir/dnote-server" \
-ldflags "-X main.versionTag=$version" \
"$projectDir"/pkg/server/*.go
packr2 clean
# build tarball
tarballName="dnote_server_${version}_${platform}_${arch}.tar.gz"
tarballPath="$outputDir/$tarballName"
cp "$projectDir/licenses/AGPLv3.txt" "$destDir"
cp "$basedir/README.md" "$destDir"
tar -C "$destDir" -zcvf "$tarballPath" "."
rm -rf "$destDir"
# calculate checksum
pushd "$outputDir"
shasum -a 256 "$tarballName" >> "$outputDir/dnote_${version}_checksums.txt"
popd
}
build linux amd64

View file

@ -0,0 +1,253 @@
/* Copyright (C) 2019 Monomax Software Pty Ltd
*
* This file is part of Dnote.
*
* Dnote is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Dnote is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Dnote. If not, see <https://www.gnu.org/licenses/>.
*/
package main
import (
"github.com/dnote/dnote/pkg/server/helpers"
"github.com/dnote/dnote/pkg/server/database"
"os"
"time"
)
func main() {
c := database.Config{
Host: os.Getenv("DBHost"),
Port: os.Getenv("DBPort"),
Name: os.Getenv("DBName"),
User: os.Getenv("DBUser"),
Password: os.Getenv("DBPassword"),
}
database.Open(c)
db := database.DBConn
tx := db.Begin()
userID, err := helpers.GetDemoUserID()
if err != nil {
panic(err)
}
var d1Notes []database.Note
var d2Notes []database.Note
var d3Notes []database.Note
var d4Notes []database.Note
var d5Notes []database.Note
var d6Notes []database.Note
var d7Notes []database.Note
var d8Notes []database.Note
var d9Notes []database.Note
var d10Notes []database.Note
var d11Notes []database.Note
var d12Notes []database.Note
if err := db.Order("random()").Limit(5).Where("user_id = ?", userID).Find(&d1Notes).Error; err != nil {
tx.Rollback()
panic(err)
}
if err := db.Order("random()").Limit(5).Where("user_id = ?", userID).Find(&d2Notes).Error; err != nil {
tx.Rollback()
panic(err)
}
if err := db.Order("random()").Limit(5).Where("user_id = ?", userID).Find(&d3Notes).Error; err != nil {
tx.Rollback()
panic(err)
}
if err := db.Order("random()").Limit(5).Where("user_id = ?", userID).Find(&d4Notes).Error; err != nil {
tx.Rollback()
panic(err)
}
if err := db.Order("random()").Limit(5).Where("user_id = ?", userID).Find(&d5Notes).Error; err != nil {
tx.Rollback()
panic(err)
}
if err := db.Order("random()").Limit(5).Where("user_id = ?", userID).Find(&d6Notes).Error; err != nil {
tx.Rollback()
panic(err)
}
if err := db.Order("random()").Limit(5).Where("user_id = ?", userID).Find(&d7Notes).Error; err != nil {
tx.Rollback()
panic(err)
}
if err := db.Order("random()").Limit(5).Where("user_id = ?", userID).Find(&d8Notes).Error; err != nil {
tx.Rollback()
panic(err)
}
if err := db.Order("random()").Limit(5).Where("user_id = ?", userID).Find(&d9Notes).Error; err != nil {
tx.Rollback()
panic(err)
}
if err := db.Order("random()").Limit(5).Where("user_id = ?", userID).Find(&d10Notes).Error; err != nil {
tx.Rollback()
panic(err)
}
if err := db.Order("random()").Limit(5).Where("user_id = ?", userID).Find(&d11Notes).Error; err != nil {
tx.Rollback()
panic(err)
}
if err := db.Order("random()").Limit(5).Where("user_id = ?", userID).Find(&d12Notes).Error; err != nil {
tx.Rollback()
panic(err)
}
d1Date := time.Date(2019, time.January, 1, 0, 0, 0, 0, time.UTC)
d1 := database.Digest{
UserID: userID,
Notes: d1Notes,
CreatedAt: d1Date,
UpdatedAt: d1Date,
}
if err := tx.Save(&d1).Error; err != nil {
tx.Rollback()
panic(err)
}
d2Date := time.Date(2019, time.February, 4, 0, 0, 0, 0, time.UTC)
d2 := database.Digest{
UserID: userID,
Notes: d2Notes,
CreatedAt: d2Date,
UpdatedAt: d2Date,
}
if err := tx.Save(&d2).Error; err != nil {
tx.Rollback()
panic(err)
}
d3Date := time.Date(2019, time.February, 12, 0, 0, 0, 0, time.UTC)
d3 := database.Digest{
UserID: userID,
Notes: d3Notes,
CreatedAt: d3Date,
UpdatedAt: d3Date,
}
if err := tx.Save(&d3).Error; err != nil {
tx.Rollback()
panic(err)
}
d4Date := time.Date(2019, time.May, 12, 0, 0, 0, 0, time.UTC)
d4 := database.Digest{
UserID: userID,
Notes: d4Notes,
CreatedAt: d4Date,
UpdatedAt: d4Date,
}
if err := tx.Save(&d4).Error; err != nil {
tx.Rollback()
panic(err)
}
d5Date := time.Date(2019, time.March, 10, 0, 0, 0, 0, time.UTC)
d5 := database.Digest{
UserID: userID,
Notes: d5Notes,
CreatedAt: d5Date,
UpdatedAt: d5Date,
}
if err := tx.Save(&d5).Error; err != nil {
tx.Rollback()
panic(err)
}
d6Date := time.Date(2019, time.February, 20, 0, 0, 0, 0, time.UTC)
d6 := database.Digest{
UserID: userID,
Notes: d6Notes,
CreatedAt: d6Date,
UpdatedAt: d6Date,
}
if err := tx.Save(&d6).Error; err != nil {
tx.Rollback()
panic(err)
}
d7Date := time.Date(2019, time.April, 24, 0, 0, 0, 0, time.UTC)
d7 := database.Digest{
UserID: userID,
Notes: d7Notes,
CreatedAt: d7Date,
UpdatedAt: d7Date,
}
if err := tx.Save(&d7).Error; err != nil {
tx.Rollback()
panic(err)
}
d8Date := time.Date(2018, time.December, 6, 0, 0, 0, 0, time.UTC)
d8 := database.Digest{
UserID: userID,
Notes: d8Notes,
CreatedAt: d8Date,
UpdatedAt: d8Date,
}
if err := tx.Save(&d8).Error; err != nil {
tx.Rollback()
panic(err)
}
d9Date := time.Date(2018, time.November, 2, 0, 0, 0, 0, time.UTC)
d9 := database.Digest{
UserID: userID,
Notes: d9Notes,
CreatedAt: d9Date,
UpdatedAt: d9Date,
}
if err := tx.Save(&d9).Error; err != nil {
tx.Rollback()
panic(err)
}
d10Date := time.Date(2018, time.October, 12, 0, 0, 0, 0, time.UTC)
d10 := database.Digest{
UserID: userID,
Notes: d10Notes,
CreatedAt: d10Date,
UpdatedAt: d10Date,
}
if err := tx.Save(&d10).Error; err != nil {
tx.Rollback()
panic(err)
}
d11Date := time.Date(2018, time.October, 1, 0, 0, 0, 0, time.UTC)
d11 := database.Digest{
UserID: userID,
Notes: d11Notes,
CreatedAt: d11Date,
UpdatedAt: d11Date,
}
if err := tx.Save(&d11).Error; err != nil {
tx.Rollback()
panic(err)
}
d12Date := time.Date(2018, time.May, 17, 0, 0, 0, 0, time.UTC)
d12 := database.Digest{
UserID: userID,
Notes: d12Notes,
CreatedAt: d12Date,
UpdatedAt: d12Date,
}
if err := tx.Save(&d12).Error; err != nil {
tx.Rollback()
panic(err)
}
tx.Commit()
}

12
scripts/server/test-local.sh Executable file
View file

@ -0,0 +1,12 @@
#!/usr/bin/env bash
# shellcheck disable=SC1090
# test-local.sh runs api tests using local setting
set -eux
dir=$(dirname "${BASH_SOURCE[0]}")
set -a
source "$dir/../../pkg/server/.env.test"
set +a
"$dir/test.sh"

19
scripts/server/test.sh Executable file
View file

@ -0,0 +1,19 @@
#!/usr/bin/env bash
# test.sh runs server tests. It is to be invoked by other scripts that set
# appropriate env vars.
set -eux
dir=$(dirname "${BASH_SOURCE[0]}")
pushd "$dir/../../pkg/server"
export DNOTE_TEST_EMAIL_TEMPLATE_DIR="$dir/../../pkg/server/mailer/templates/src"
if [ "${WATCH-false}" == true ]; then
set +e
while inotifywait --exclude .swp -e modify -r .; do go test ./... -cover -p 1; done;
set -e
else
go test ./... -cover -p 1
fi
popd

21
scripts/web/build-prod.sh Executable file
View file

@ -0,0 +1,21 @@
#!/usr/bin/env bash
# build.sh builds a production bundle
set -eux
dir=$(dirname "${BASH_SOURCE[0]}")
basePath="$dir/../.."
publicPath="$basePath/web/public"
compiledPath="$basePath/web/compiled"
bundleBaseUrl="/static"
assetBaseUrl="/static"
rootUrl=""
BUNDLE_BASE_URL="$bundleBaseUrl" \
ASSET_BASE_URL="$assetBaseUrl" \
ROOT_URL="$rootUrl" \
PUBLIC_PATH="$publicPath" \
COMPILED_PATH="$compiledPath" \
VERSION="$VERSION" \
"$dir/build.sh"

39
scripts/web/build.sh Executable file
View file

@ -0,0 +1,39 @@
#!/usr/bin/env bash
# build.sh builds a bundle
set -ex
dir=$(dirname "${BASH_SOURCE[0]}")
basePath="$dir/../.."
isTest=${IS_TEST:-false}
set -u
rm -rf "$basePath/web/public"
mkdir -p "$basePath/web/public/static"
pushd "$basePath/web"
PUBLIC_PATH="$PUBLIC_PATH" \
COMPILED_PATH="$COMPILED_PATH" \
ASSET_BASE_URL="$ASSET_BASE_URL" \
"$dir/setup.sh"
OUTPUT_PATH="$COMPILED_PATH" \
ROOT_URL="$ROOT_URL" \
VERSION="$VERSION" \
"$basePath"/web/node_modules/.bin/webpack\
--colors\
--display-error-details\
--env.isTest="$isTest"\
--config "$basePath"/web/webpack/prod.config.js
NODE_ENV=PRODUCTION \
BUNDLE_BASE_URL=$BUNDLE_BASE_URL \
ASSET_BASE_URL=$ASSET_BASE_URL \
PUBLIC_PATH=$PUBLIC_PATH \
COMPILED_PATH=$COMPILED_PATH \
node "$dir/placeholder.js"
cp "$COMPILED_PATH"/*.js "$COMPILED_PATH"/*.css "$PUBLIC_PATH"/static
# clean up compiled
rm -rf "$basePath"/web/compiled/*
popd

40
scripts/web/dev.sh Executable file
View file

@ -0,0 +1,40 @@
#!/usr/bin/env bash
# shellcheck disable=SC1090
# dev.sh builds and starts development environment
set -eux -o pipefail
# clean up background processes
function cleanup {
kill "$devServerPID"
}
trap cleanup EXIT
dir=$(dirname "${BASH_SOURCE[0]}")
basePath="$dir/../.."
appPath="$basePath/web"
serverPath="$basePath/pkg/server"
serverPort=3000
# load env
set -a
dotenvPath="$serverPath/.env.dev"
source "$dotenvPath"
set +a
# run webpack-dev-server for js in the background
(
BUNDLE_BASE_URL=http://localhost:8080 \
ASSET_BASE_URL=http://localhost:3000/static \
ROOT_URL=http://localhost:$serverPort \
COMPILED_PATH="$appPath"/compiled \
PUBLIC_PATH="$appPath"/public \
COMPILED_PATH="$basePath/web/compiled" \
IS_TEST=true \
VERSION="$VERSION" \
WEBPACK_HOST="0.0.0.0" \
"$dir/webpack-dev.sh"
) &
devServerPID=$!
# run server
(cd "$serverPath/watcher" && go run main.go)

102
scripts/web/placeholder.js Normal file
View file

@ -0,0 +1,102 @@
/* Copyright (C) 2019 Monomax Software Pty Ltd
*
* This file is part of Dnote.
*
* Dnote is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Dnote is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Dnote. If not, see <https://www.gnu.org/licenses/>.
*/
// placeholder.js replaces the placeholders in index.html with real values
// It is needed to load assets whose paths are not fixed because they change
// every time they are generated.
const fs = require('fs');
const path = require('path');
// bundleBaseURL is the base URL from which the application javascript bundle
// In production, it should be the same as assetBaseURL. It is used for development
// environment, in which it is configured to be the webpack development server.
const bundleBaseURL = process.env.BUNDLE_BASE_URL;
// assetBaseURL is the base URL from which all assets excluding the application
// bundle is served.
const assetBaseURL = process.env.ASSET_BASE_URL;
const publicPath = process.env.PUBLIC_PATH;
const compiledPath = process.env.COMPILED_PATH;
if (bundleBaseURL === undefined) {
throw new Error('No BUNDLE_BASE_URL environment variable found');
}
if (assetBaseURL === undefined) {
throw new Error('No ASSET_BASE_URL environment variable found');
}
if (publicPath === undefined) {
throw new Error('No PUBLIC_PATH environment variable found');
}
if (compiledPath === undefined) {
throw new Error('No COMPILED_PATH environment variable found');
}
const isProduction = process.env.NODE_ENV === 'PRODUCTION';
const indexHtmlPath = `${publicPath}/index.html`;
const assetManifestPath = path.resolve(compiledPath, 'webpack-manifest.json');
let manifest;
try {
// eslint-disable-next-line import/no-dynamic-require,global-require
manifest = require(assetManifestPath);
// eslint-disable-next-line no-empty
} catch (e) {
if (isProduction) {
throw new Error('asset manifest not found');
}
}
function getJSBundleTag() {
let jsFilename;
if (isProduction) {
jsFilename = manifest['app.js'];
} else {
jsFilename = '/app.js';
}
const jsBundleUrl = `${bundleBaseURL}${jsFilename}`;
return `<script src="${jsBundleUrl}"></script>`;
}
// Replace the placeholders with real values
fs.readFile(indexHtmlPath, 'utf8', (err, data) => {
if (err) {
console.log('Error while reading index.html');
console.log(err);
process.exit(1);
}
const jsBundleTag = getJSBundleTag();
let result = data.replace(/<!--JS_BUNDLE_PLACEHOLDER-->/g, jsBundleTag);
result = result.replace(/<!--ASSET_BASE_PLACEHOLDER-->/g, assetBaseURL);
if (isProduction) {
const cssBundleUrl = `${assetBaseURL}${manifest['app.css']}`;
const cssBundleTag = `<link rel="stylesheet" href="${cssBundleUrl}" />`;
result = result.replace(/<!--CSS_BUNDLE_PLACEHOLDER-->/g, cssBundleTag);
}
fs.writeFile(indexHtmlPath, result, 'utf8', writeErr => {
if (writeErr) {
console.log('Error while writing index.html');
console.log(writeErr);
process.exit(1);
}
});
});

22
scripts/web/setup.sh Executable file
View file

@ -0,0 +1,22 @@
#!/usr/bin/env bash
# setup.sh prepares the directory structure and copies static files
set -eux -o pipefail
dir=$(dirname "${BASH_SOURCE[0]}")
basePath="$dir/../.."
publicPath=$PUBLIC_PATH
compiledPath=$COMPILED_PATH
assetBaseUrl=$ASSET_BASE_URL
# prepare directories
rm -rf "$compiledPath"
rm -rf "$publicPath"
mkdir -p "$compiledPath"
mkdir -p "$publicPath"
# copy the assets and artifacts
cp -r "$basePath"/web/assets/* "$publicPath"
# populate placeholders
assetBaseUrlEscaped=$(echo "$assetBaseUrl" | sed -e 's/[\/&]/\\&/g')
sed -i -e "s/ASSET_BASE_PLACEHOLDER/$assetBaseUrlEscaped/g" "$publicPath"/static/manifest.json

28
scripts/web/webpack-dev.sh Executable file
View file

@ -0,0 +1,28 @@
#!/usr/bin/env bash
set -eux
dir=$(dirname "${BASH_SOURCE[0]}")
basePath="$dir/../.."
appPath="$basePath/web"
(
cd "$appPath" &&
PUBLIC_PATH=$PUBLIC_PATH \
COMPILED_PATH=$COMPILED_PATH \
ASSET_BASE_URL=$ASSET_BASE_URL \
"$dir/setup.sh" &&
BUNDLE_BASE_URL=$BUNDLE_BASE_URL
ASSET_BASE_URL=$ASSET_BASE_URL \
COMPILED_PATH=$COMPILED_PATH \
PUBLIC_PATH=$PUBLIC_PATH \
IS_TEST=true \
node "$dir/placeholder.js" &&
ROOT_URL=$ROOT_URL \
VERSION="$VERSION" \
"$appPath"/node_modules/.bin/webpack-dev-server\
--env.isTest="$IS_TEST" \
--host "$WEBPACK_HOST" \
--config "$appPath"/webpack/dev.config.js
)