Compare commits
9 Commits
Author | SHA1 | Date | |
---|---|---|---|
95908c9290 | |||
3c9c24f30b | |||
16c7852972 | |||
0c7ab9524d | |||
7fb26bab97 | |||
ca70773a8c | |||
a0f9670688 | |||
2fd45ac35c | |||
3844e97c48 |
2
.gitignore
vendored
2
.gitignore
vendored
@ -3,6 +3,8 @@ api/personal_measure_api
|
|||||||
api/postgres.container.id
|
api/postgres.container.id
|
||||||
api/src/main/nim/personal_measure_api
|
api/src/main/nim/personal_measure_api
|
||||||
api/src/main/nim/personal_measure_apipkg/db
|
api/src/main/nim/personal_measure_apipkg/db
|
||||||
|
api/personal_measure_api.config.dev.json
|
||||||
|
api/personal_measure_api.config.prod.json
|
||||||
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
node_modules
|
node_modules
|
||||||
|
3
Makefile
3
Makefile
@ -9,6 +9,9 @@ clean:
|
|||||||
-docker container prune
|
-docker container prune
|
||||||
-docker image prune
|
-docker image prune
|
||||||
|
|
||||||
|
update-version:
|
||||||
|
operations/update-version.sh
|
||||||
|
|
||||||
dist/personal-measure-web.tar.gz:
|
dist/personal-measure-web.tar.gz:
|
||||||
-mkdir dist
|
-mkdir dist
|
||||||
TARGET_ENV=$(TARGET_ENV) make -C web build
|
TARGET_ENV=$(TARGET_ENV) make -C web build
|
||||||
|
@ -16,7 +16,7 @@ RUN apk -v --update add --no-cache \
|
|||||||
postgresql-client
|
postgresql-client
|
||||||
|
|
||||||
COPY --from=build /pm-api/personal_measure_api /
|
COPY --from=build /pm-api/personal_measure_api /
|
||||||
COPY personal_measure_api.config.prod.json /personal_measure_api.config.json
|
COPY personal_measure_api.config.docker.json /personal_measure_api.config.json
|
||||||
CMD ["/personal_measure_api", "serve"]
|
CMD ["/personal_measure_api", "serve"]
|
||||||
|
|
||||||
# TODO: replace the above with something like:
|
# TODO: replace the above with something like:
|
||||||
|
108
api/Makefile
108
api/Makefile
@ -1,45 +1,123 @@
|
|||||||
PGSQL_CONTAINER_ID=`cat postgres.container.id`
|
PGSQL_CONTAINER_ID=`cat postgres.container.id`
|
||||||
ECR_ACCOUNT_URL=063932952339.dkr.ecr.us-west-2.amazonaws.com
|
|
||||||
DB_NAME="personal_measure"
|
|
||||||
VERSION=`git describe`
|
|
||||||
SOURCES=$(wildcard src/main/nim/*.nim) $(wildcard src/main/nim/personal_measure_apipkg/*.nim)
|
SOURCES=$(wildcard src/main/nim/*.nim) $(wildcard src/main/nim/personal_measure_apipkg/*.nim)
|
||||||
|
|
||||||
serve-local: personal_measure_api start-postgres
|
# Variables that can be overriden
|
||||||
|
# -------------------------------
|
||||||
|
|
||||||
|
# AWS Account URL for the ECR repository
|
||||||
|
ECR_ACCOUNT_URL ?= 063932952339.dkr.ecr.us-west-2.amazonaws.com
|
||||||
|
|
||||||
|
# The version number that will be tagged the container image. You might want to
|
||||||
|
# override this when doing local development to create local versions that are
|
||||||
|
# reflect changes not yet committed.
|
||||||
|
VERSION ?= `git describe`
|
||||||
|
|
||||||
|
# The port on the host machine (not the container)
|
||||||
|
PORT ?= 8100
|
||||||
|
|
||||||
|
# The name of the database (used then creating a local Postgres container)
|
||||||
|
DB_NAME ?= personal_measure
|
||||||
|
|
||||||
|
# The database connection string. You would change this to point the API at a
|
||||||
|
# different database server (default is the local Postgres container).
|
||||||
|
DB_CONN_STRING ?= host=localhost dbname=$(DB_NAME) user=postgres password=password port=5500
|
||||||
|
|
||||||
|
# The API authentication secret (used for hashing passwords, etc.)
|
||||||
|
AUTH_SECRET ?= 123abc
|
||||||
|
|
||||||
|
|
||||||
|
default: start-postgres serve-docker
|
||||||
|
|
||||||
|
# Building and deploying the API container image
|
||||||
|
# ----------------------------------------------
|
||||||
|
|
||||||
|
personal_measure_api-image: $(SOURCES)
|
||||||
|
# Build the container image.
|
||||||
|
docker image build -t $(ECR_ACCOUNT_URL)/personal_measure_api:$(VERSION) .
|
||||||
|
|
||||||
|
push-image: personal_measure_api-image
|
||||||
|
# Push the container image to the private AWS ECR
|
||||||
|
docker push $(ECR_ACCOUNT_URL)/personal_measure_api:$(VERSION)
|
||||||
|
|
||||||
|
# Running the API locally on bare metal
|
||||||
|
# -------------------------------------
|
||||||
|
|
||||||
|
personal_measure_api: $(SOURCES)
|
||||||
|
# Build the API
|
||||||
|
nimble build
|
||||||
|
|
||||||
|
serve: personal_measure_api
|
||||||
|
# Run the API on this machine. Note that configuration is taken by default
|
||||||
|
# from the `personal_measure_api.config.json` file, but environment variables
|
||||||
|
# specified when running make can be used to override these (to change the
|
||||||
|
# DB_CONN_STRING, for example).
|
||||||
./personal_measure_api serve
|
./personal_measure_api serve
|
||||||
|
|
||||||
serve-docker: personal_measure_api-image start-postgres
|
# Running the API locally in a container
|
||||||
docker run -e AUTH_SECRET=abc123 -e "DB_CONN_STRING=host=host.docker.internal port=5500 user=postgres password=password dbname=personal_measure" -e PORT=80 -p 127.0.0.1:8100:80/tcp $(ECR_ACCOUNT_URL)/personal_measure_api:$(VERSION)
|
# --------------------------------------
|
||||||
|
|
||||||
|
serve-docker: personal_measure_api-image
|
||||||
|
# Run the API in a docker container. Note that the configuration loaded into
|
||||||
|
# the Docker container defines very little of the actual configuration as
|
||||||
|
# environment variables are used in the deployed environments. Accordingly,
|
||||||
|
# we must specify them explicitly here.
|
||||||
|
docker run \
|
||||||
|
-e AUTH_SECRET=$(AUTH_SECRET) \
|
||||||
|
-e PORT=80 \
|
||||||
|
-e "DB_CONN_STRING=$(DB_CONN_STRING)" \
|
||||||
|
-e 'KNOWN_ORIGINS=["https://curl.localhost"]' \
|
||||||
|
-p 127.0.0.1:$(PORT):80/tcp \
|
||||||
|
$(ECR_ACCOUNT_URL)/personal_measure_api:$(VERSION)
|
||||||
|
|
||||||
|
# Managing Postgres in a local container
|
||||||
|
# --------------------------------------
|
||||||
|
#
|
||||||
|
# This supports local development on this machine. These commands rely on a
|
||||||
|
# file named `postgres.container.id` to track the existing and ID of the
|
||||||
|
# local Postgres instance.
|
||||||
|
|
||||||
postgres.container.id:
|
postgres.container.id:
|
||||||
|
# This creates a new local Postegres container and initializes the PM API
|
||||||
|
# database scheme.
|
||||||
docker run --name postgres-$(DB_NAME) -e POSTGRES_PASSWORD=password -p 5500:5432 -d postgres > postgres.container.id
|
docker run --name postgres-$(DB_NAME) -e POSTGRES_PASSWORD=password -p 5500:5432 -d postgres > postgres.container.id
|
||||||
sleep 5
|
sleep 5
|
||||||
PGPASSWORD=password psql -p 5500 -U postgres -h localhost -c "CREATE DATABASE $(DB_NAME);"
|
PGPASSWORD=password psql -p 5500 -U postgres -h localhost -c "CREATE DATABASE $(DB_NAME);"
|
||||||
db_migrate up -c database-local.json
|
db_migrate up -c database-local.json
|
||||||
|
|
||||||
start-postgres: postgres.container.id
|
start-postgres: postgres.container.id
|
||||||
|
# Start the existing local Postgres container
|
||||||
docker start $(PGSQL_CONTAINER_ID)
|
docker start $(PGSQL_CONTAINER_ID)
|
||||||
sleep 1
|
sleep 1
|
||||||
db_migrate up -c database-local.json
|
db_migrate up -c database-local.json
|
||||||
|
|
||||||
stop-postgres: postgres.container.id
|
stop-postgres: postgres.container.id
|
||||||
|
# Stop the existing local Postgres container
|
||||||
docker stop $(PGSQL_CONTAINER_ID)
|
docker stop $(PGSQL_CONTAINER_ID)
|
||||||
|
|
||||||
delete-postgres-container:
|
delete-postgres-container:
|
||||||
|
# Delete the local Postgres container. Note that this will destroy any data
|
||||||
|
# in this database instance.
|
||||||
-docker stop $(PGSQL_CONTAINER_ID)
|
-docker stop $(PGSQL_CONTAINER_ID)
|
||||||
docker container rm $(PGSQL_CONTAINER_ID)
|
docker container rm $(PGSQL_CONTAINER_ID)
|
||||||
rm postgres.container.id
|
rm postgres.container.id
|
||||||
|
|
||||||
connect:
|
connect-postgres:
|
||||||
|
# Connect to the Postgres instance running in the local container
|
||||||
PGPASSWORD=password psql -p 5500 -U postgres -h localhost ${DB_NAME}
|
PGPASSWORD=password psql -p 5500 -U postgres -h localhost ${DB_NAME}
|
||||||
|
|
||||||
|
|
||||||
|
# Utility
|
||||||
|
# -------
|
||||||
|
|
||||||
ecr-auth:
|
ecr-auth:
|
||||||
|
# Authenticate docker to the AWS private elastic container repository.
|
||||||
aws ecr get-login-password --region us-west-2 | docker login --username AWS --password-stdin 063932952339.dkr.ecr.us-west-2.amazonaws.com
|
aws ecr get-login-password --region us-west-2 | docker login --username AWS --password-stdin 063932952339.dkr.ecr.us-west-2.amazonaws.com
|
||||||
|
|
||||||
personal_measure_api: $(SOURCES)
|
echo-vars:
|
||||||
nimble build
|
@echo \
|
||||||
|
" ECR_ACCOUNT_URL=$(ECR_ACCOUNT_URL)\n" \
|
||||||
personal_measure_api-image: $(SOURCES)
|
"VERSION=$(VERSION)\n" \
|
||||||
docker image build -t $(ECR_ACCOUNT_URL)/personal_measure_api:$(VERSION) .
|
"PORT=$(PORT)\n" \
|
||||||
|
"DB_NAME=$(DB_NAME)\n" \
|
||||||
push-image: personal_measure_api-image
|
"DB_CONN_STRING=$(DB_CONN_STRING)\n" \
|
||||||
docker push $(ECR_ACCOUNT_URL)/personal_measure_api:$(VERSION)
|
"AUTH_SECRET=$(AUTH_SECRET)\n"
|
||||||
|
30
api/README.md
Normal file
30
api/README.md
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
## Local Development
|
||||||
|
|
||||||
|
Examples of different local development & testing scenarios:
|
||||||
|
|
||||||
|
- Bare-metal API server, local Postgres container
|
||||||
|
|
||||||
|
make start-postgres
|
||||||
|
make serve
|
||||||
|
|
||||||
|
- Bare-metal API server, different Postgres server
|
||||||
|
|
||||||
|
DB_CONN_STRING="host=<db-hostname> user=pmapi password=<pwd>" make serve
|
||||||
|
|
||||||
|
- Docker API Server, local Postgres container
|
||||||
|
|
||||||
|
make start-postgres
|
||||||
|
VERSION=0.X.0-alpha make serve-docker
|
||||||
|
|
||||||
|
- Docker API server, different Postgres server
|
||||||
|
|
||||||
|
DB_CONN_STRING="host=<db-hostname> user=pmapi password=<pwd>" \
|
||||||
|
VERSION=0.X.0-alpha \
|
||||||
|
make serve-docker
|
||||||
|
|
||||||
|
All of the available `make` targets are documented inline; see the
|
||||||
|
[Makefile](./Makefile) for more details.
|
||||||
|
|
||||||
|
### Using the API CLI wrapper
|
||||||
|
|
||||||
|
The API CLI wrapper
|
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"driver": "postgres",
|
"driver": "postgres",
|
||||||
"connectionString": "host=localhost port=5999 dbname=personal_measure_dev user=postgres",
|
"connectionString": "host=localhost port=5432 dbname=personal_measure_dev user=pmapi",
|
||||||
"sqlDir": "src/main/sql/migrations"
|
"sqlDir": "src/main/sql/migrations"
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"driver": "postgres",
|
"driver": "postgres",
|
||||||
"connectionString": "host=localhost port=5999 dbname=personal_measure user=postgres",
|
"connectionString": "host=localhost port=5432 dbname=personal_measure user=pmapi",
|
||||||
"sqlDir": "src/main/sql/migrations"
|
"sqlDir": "src/main/sql/migrations"
|
||||||
}
|
}
|
||||||
|
4
api/personal_measure_api.config.docker.json
Normal file
4
api/personal_measure_api.config.docker.json
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"debug":false,
|
||||||
|
"pwdCost":11
|
||||||
|
}
|
@ -2,7 +2,7 @@
|
|||||||
"authSecret":"bifekHuffIs3",
|
"authSecret":"bifekHuffIs3",
|
||||||
"dbConnString":"host=localhost port=5500 dbname=personal_measure user=postgres password=password",
|
"dbConnString":"host=localhost port=5500 dbname=personal_measure user=postgres password=password",
|
||||||
"debug":true,
|
"debug":true,
|
||||||
"port":8081,
|
"port":8100,
|
||||||
"pwdCost":11,
|
"pwdCost":11,
|
||||||
"knownOrigins": [ "https://curl.localhost" ]
|
"knownOrigins": [ "https://curl.localhost" ]
|
||||||
}
|
}
|
||||||
|
@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"debug":false,
|
|
||||||
"pwdCost":11,
|
|
||||||
"knownOrigins": [ "https://pm.jdb-software.com", "https://pm-dev.jdb-software.com" ]
|
|
||||||
}
|
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
include "src/main/nim/personal_measure_apipkg/version.nim"
|
include "src/main/nim/personal_measure_apipkg/version.nim"
|
||||||
|
|
||||||
version = "0.10.0"
|
version = "0.11.0"
|
||||||
author = "Jonathan Bernard"
|
author = "Jonathan Bernard"
|
||||||
description = "JDB\'s Personal Measures API"
|
description = "JDB\'s Personal Measures API"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
@ -18,4 +18,4 @@ requires @["nim >= 0.19.4", "bcrypt", "docopt >= 0.6.8", "isaac >= 0.1.3",
|
|||||||
|
|
||||||
requires "https://git.jdb-software.com/jdb/nim-cli-utils.git >= 0.6.3"
|
requires "https://git.jdb-software.com/jdb/nim-cli-utils.git >= 0.6.3"
|
||||||
requires "https://git.jdb-software.com/jdb/nim-time-utils.git >= 0.5.2"
|
requires "https://git.jdb-software.com/jdb/nim-time-utils.git >= 0.5.2"
|
||||||
requires "https://git.jdb-software.com/jdb-software/fiber-orm-nim.git >= 0.3.1"
|
requires "https://git.jdb-software.com/jdb-software/fiber-orm-nim.git >= 0.3.2"
|
||||||
|
@ -34,19 +34,15 @@ proc loadConfig*(args: Table[string, docopt.Value] = initTable[string, docopt.Va
|
|||||||
warn "Cannot read configuration file \"" & filePath & "\":\n\t" &
|
warn "Cannot read configuration file \"" & filePath & "\":\n\t" &
|
||||||
getCurrentExceptionMsg()
|
getCurrentExceptionMsg()
|
||||||
|
|
||||||
let knownOriginsArray =
|
|
||||||
if json.hasKey("knownOrigins"): json["knownOrigins"]
|
|
||||||
else: newJArray()
|
|
||||||
|
|
||||||
let cfg = CombinedConfig(docopt: args, json: json)
|
let cfg = CombinedConfig(docopt: args, json: json)
|
||||||
|
|
||||||
result = PMApiConfig(
|
result = PMApiConfig(
|
||||||
authSecret: cfg.getVal("auth-secret"),
|
authSecret: cfg.getVal("auth-secret"),
|
||||||
dbConnString: cfg.getVal("db-conn-string"),
|
dbConnString: cfg.getVal("db-conn-string"),
|
||||||
debug: "true".startsWith(cfg.getVal("debug", "false").toLower()),
|
debug: "true".startsWith(cfg.getVal("debug", "false").toLower()),
|
||||||
port: parseInt(cfg.getVal("port", "8080")),
|
port: parseInt(cfg.getVal("port", "8100")),
|
||||||
pwdCost: cast[int8](parseInt(cfg.getVal("pwd-cost", "11"))),
|
pwdCost: cast[int8](parseInt(cfg.getVal("pwd-cost", "11"))),
|
||||||
knownOrigins: toSeq(knownOriginsArray).mapIt(it.getStr))
|
knownOrigins: cfg.getVal("known-origins")[1..^2].split(',').mapIt(it[1..^2]))
|
||||||
|
|
||||||
proc initContext(args: Table[string, docopt.Value]): PMApiContext =
|
proc initContext(args: Table[string, docopt.Value]): PMApiContext =
|
||||||
|
|
||||||
|
@ -1 +1 @@
|
|||||||
const PM_API_VERSION* = "0.10.0"
|
const PM_API_VERSION* = "0.11.0"
|
||||||
|
@ -1,24 +1,37 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
api_base_url="${PM_API_BASE_URL:-http://localhost:8081}"
|
api_base_url="${PM_API_BASE_URL:-http://localhost:8100/v0}"
|
||||||
if [ $# -eq 1 ]; then
|
if [ $# -eq 1 ]; then
|
||||||
url="$1"
|
url="$1"
|
||||||
method="GET"
|
method="GET"
|
||||||
data=""
|
data=""
|
||||||
elif [ $# -eq 2 ]; then
|
elif [ $# -eq 2 ]; then
|
||||||
method="$1"
|
if [ $1 == "auth-token" ]; then
|
||||||
url="$2"
|
curl -s -X POST \
|
||||||
data=""
|
-H "Origin: https://curl.localhost" \
|
||||||
else
|
"${api_base_url}/auth-token" \
|
||||||
|
-d "$2" \
|
||||||
|
| xargs printf "Bearer %s" \
|
||||||
|
> credential
|
||||||
|
|
||||||
|
exit 0
|
||||||
|
else
|
||||||
|
method="$1"
|
||||||
|
url="$2"
|
||||||
|
data=""
|
||||||
|
fi
|
||||||
|
else
|
||||||
method="$1"
|
method="$1"
|
||||||
url="$2"
|
url="$2"
|
||||||
data="$3"
|
data="$3"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [[ ! $url = /* ]]; then url="/$url"; fi
|
||||||
|
|
||||||
curl -s -X "$method" \
|
curl -s -X "$method" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-H "Authorization: $(cat credential)" \
|
-H "Authorization: $(cat credential)" \
|
||||||
-H "Origin: https://curl.localhost" \
|
-H "Origin: https://curl.localhost" \
|
||||||
"${api_base_url}/api/$url" \
|
"${api_base_url}$url" \
|
||||||
-d "$data" \
|
-d "$data" \
|
||||||
-v
|
| jq .
|
||||||
|
@ -1 +1 @@
|
|||||||
008
|
010
|
||||||
|
3
doc/issues/open/008-toggle-measure-visibility.md
Normal file
3
doc/issues/open/008-toggle-measure-visibility.md
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
### Toggle Measure Visibility
|
||||||
|
|
||||||
|
Allow the user to choose whether a measure should be visible or hidden by default.
|
3
doc/issues/open/009-grouped-measures.md
Normal file
3
doc/issues/open/009-grouped-measures.md
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
### Grouped Measures
|
||||||
|
|
||||||
|
Create a measure type that is just a grouping of several other measures. For example, it would be nice to be able to group all workout-related measures into one group. The graph could show an overlay of all the different measures on one graph.
|
@ -1,10 +1,5 @@
|
|||||||
resource "aws_secretsmanager_secret" "pmapi_auth" {
|
resource "aws_secretsmanager_secret" "pmapi" {
|
||||||
name = "${local.environment_name}-AuthSecret"
|
name = "${local.environment_name}-Config"
|
||||||
tags = { Environment = local.environment_name }
|
|
||||||
}
|
|
||||||
|
|
||||||
resource "aws_secretsmanager_secret" "pmapi_db_conn_string" {
|
|
||||||
name = "${local.environment_name}-DbConnString"
|
|
||||||
tags = { Environment = local.environment_name }
|
tags = { Environment = local.environment_name }
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -38,12 +33,17 @@ resource "aws_ecs_task_definition" "pmapi" {
|
|||||||
{
|
{
|
||||||
name = "AUTH_SECRET"
|
name = "AUTH_SECRET"
|
||||||
description = "Auth secret used to hash and salt passwords."
|
description = "Auth secret used to hash and salt passwords."
|
||||||
valueFrom = aws_secretsmanager_secret.pmapi_auth.arn
|
valueFrom = "${aws_secretsmanager_secret.pmapi.arn}:authSecret::"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name = "DB_CONN_STRING"
|
name = "DB_CONN_STRING"
|
||||||
description = "Connection string with user credentials."
|
description = "Connection string with user credentials."
|
||||||
valueFrom = aws_secretsmanager_secret.pmapi_db_conn_string.arn
|
valueFrom = "${aws_secretsmanager_secret.pmapi.arn}:dbConnString::"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name = "KNOWN_ORIGINS"
|
||||||
|
description = "Connection string with user credentials."
|
||||||
|
valueFrom = "${aws_secretsmanager_secret.pmapi.arn}:knownOrigins::"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -27,8 +27,7 @@ resource "aws_iam_role" "ecs_task" {
|
|||||||
"kms:Decrypt"
|
"kms:Decrypt"
|
||||||
]
|
]
|
||||||
Resource = [
|
Resource = [
|
||||||
aws_secretsmanager_secret.pmapi_auth.arn,
|
aws_secretsmanager_secret.pmapi.arn
|
||||||
aws_secretsmanager_secret.pmapi_db_conn_string.arn
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
2
web/package-lock.json
generated
2
web/package-lock.json
generated
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "personal-measure-web",
|
"name": "personal-measure-web",
|
||||||
"version": "0.10.0",
|
"version": "0.11.0",
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "personal-measure-web",
|
"name": "personal-measure-web",
|
||||||
"version": "0.10.0",
|
"version": "0.11.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"serve": "npx servor dist",
|
"serve": "npx servor dist",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user