Adds golden output dirs for E2E testing (Part 2) (#475)

This commit is contained in:
Shayne Czyzewski 2022-03-01 15:42:31 -05:00 committed by GitHub
parent 84821005f4
commit 4ecc46eab3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
159 changed files with 3985 additions and 2 deletions

View File

@ -1,3 +1 @@
*-current
# TODO: undo below once PR clean and generate golden dirs for a second PR
*-golden

View File

@ -0,0 +1,47 @@
waspBuild/.wasp/build/.dockerignore
waspBuild/.wasp/build/Dockerfile
waspBuild/.wasp/build/db/schema.prisma
waspBuild/.wasp/build/installedFullStackNpmDependencies.json
waspBuild/.wasp/build/server/.npmrc
waspBuild/.wasp/build/server/.nvmrc
waspBuild/.wasp/build/server/README.md
waspBuild/.wasp/build/server/package.json
waspBuild/.wasp/build/server/src/app.js
waspBuild/.wasp/build/server/src/config.js
waspBuild/.wasp/build/server/src/core/AuthError.js
waspBuild/.wasp/build/server/src/core/HttpError.js
waspBuild/.wasp/build/server/src/dbClient.js
waspBuild/.wasp/build/server/src/ext-src/Main.css
waspBuild/.wasp/build/server/src/ext-src/MainPage.js
waspBuild/.wasp/build/server/src/ext-src/waspLogo.png
waspBuild/.wasp/build/server/src/routes/index.js
waspBuild/.wasp/build/server/src/routes/operations/index.js
waspBuild/.wasp/build/server/src/server.js
waspBuild/.wasp/build/server/src/utils.js
waspBuild/.wasp/build/web-app/README.md
waspBuild/.wasp/build/web-app/netlify.toml
waspBuild/.wasp/build/web-app/package.json
waspBuild/.wasp/build/web-app/public/favicon.ico
waspBuild/.wasp/build/web-app/public/index.html
waspBuild/.wasp/build/web-app/public/manifest.json
waspBuild/.wasp/build/web-app/src/api.js
waspBuild/.wasp/build/web-app/src/config.js
waspBuild/.wasp/build/web-app/src/ext-src/Main.css
waspBuild/.wasp/build/web-app/src/ext-src/MainPage.js
waspBuild/.wasp/build/web-app/src/ext-src/waspLogo.png
waspBuild/.wasp/build/web-app/src/index.css
waspBuild/.wasp/build/web-app/src/index.js
waspBuild/.wasp/build/web-app/src/logo.png
waspBuild/.wasp/build/web-app/src/operations/index.js
waspBuild/.wasp/build/web-app/src/operations/resources.js
waspBuild/.wasp/build/web-app/src/queries/index.js
waspBuild/.wasp/build/web-app/src/queryCache.js
waspBuild/.wasp/build/web-app/src/router.js
waspBuild/.wasp/build/web-app/src/serviceWorker.js
waspBuild/.wasp/build/web-app/src/utils.js
waspBuild/.wasproot
waspBuild/ext/.waspignore
waspBuild/ext/Main.css
waspBuild/ext/MainPage.js
waspBuild/ext/waspLogo.png
waspBuild/main.wasp

View File

@ -0,0 +1,2 @@
**/node_modules/
**/.git

View File

@ -0,0 +1,27 @@
FROM node:14-alpine AS node
FROM node AS base
RUN apk --no-cache -U upgrade # To ensure any potential security patches are applied.
FROM base AS server-builder
# Install packages needed to build native npm packages.
RUN apk add --no-cache build-base libtool autoconf automake python
WORKDIR /app
# Install npm packages, resulting in node_modules/.
COPY server/package*.json ./server/
RUN cd server && npm install
# TODO: Use pm2?
# TODO: Use non-root user (node).
FROM base AS server-production
ENV NODE_ENV production
WORKDIR /app
COPY --from=server-builder /app/server/node_modules ./server/node_modules
COPY server/ ./server/
COPY db/ ./db/
EXPOSE ${PORT}
WORKDIR /app/server
ENTRYPOINT ["npm", "run", "start-production"]

View File

@ -0,0 +1,11 @@
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
output = "../server/node_modules/.prisma/client"
}

View File

@ -0,0 +1 @@
{"npmDepsForServer":{"dependencies":[{"name":"cookie-parser","version":"~1.4.4"},{"name":"cors","version":"^2.8.5"},{"name":"debug","version":"~2.6.9"},{"name":"express","version":"~4.16.1"},{"name":"morgan","version":"~1.9.1"},{"name":"@prisma/client","version":"3.9.1"},{"name":"jsonwebtoken","version":"^8.5.1"},{"name":"secure-password","version":"^4.0.0"},{"name":"dotenv","version":"8.2.0"},{"name":"helmet","version":"^4.6.0"}],"devDependencies":[{"name":"nodemon","version":"^2.0.4"},{"name":"standard","version":"^14.3.4"},{"name":"prisma","version":"3.9.1"}]},"npmDepsForWebApp":{"dependencies":[{"name":"axios","version":"^0.21.1"},{"name":"lodash","version":"^4.17.15"},{"name":"react","version":"^16.12.0"},{"name":"react-dom","version":"^16.12.0"},{"name":"react-query","version":"^2.14.1"},{"name":"react-router-dom","version":"^5.1.2"},{"name":"react-scripts","version":"4.0.3"},{"name":"uuid","version":"^3.4.0"}],"devDependencies":[]}}

View File

@ -0,0 +1 @@
engine-strict=true

View File

@ -0,0 +1,5 @@
`npm start` to run server in development mode (it reloads on changes).
`npm run debug` to run `npm start` with debug logs enabled.
`npm run standard` to run StandardJS.

View File

@ -0,0 +1,37 @@
{
"dependencies": {
"@prisma/client": "3.9.1",
"cookie-parser": "~1.4.4",
"cors": "^2.8.5",
"debug": "~2.6.9",
"dotenv": "8.2.0",
"express": "~4.16.1",
"helmet": "^4.6.0",
"jsonwebtoken": "^8.5.1",
"morgan": "~1.9.1",
"secure-password": "^4.0.0"
},
"devDependencies": {
"nodemon": "^2.0.4",
"prisma": "3.9.1",
"standard": "^14.3.4"
},
"engines": {
"node": ">=12.18.0"
},
"name": "server",
"nodemonConfig": {
"delay": "1000"
},
"private": true,
"scripts": {
"db-migrate-dev": "prisma migrate dev --schema=../db/schema.prisma",
"db-migrate-prod": "prisma migrate deploy --schema=../db/schema.prisma",
"debug": "DEBUG=server:* npm start",
"standard": "standard",
"start": "nodemon -r dotenv/config ./src/server.js",
"start-production": "NODE_ENV=production node ./src/server.js"
},
"type": "module",
"version": "0.0.0"
}

View File

@ -0,0 +1,37 @@
import express from 'express'
import cookieParser from 'cookie-parser'
import logger from 'morgan'
import cors from 'cors'
import helmet from 'helmet'
import HttpError from './core/HttpError.js'
import indexRouter from './routes/index.js'
// TODO: Consider extracting most of this logic into createApp(routes, path) function so that
// it can be used in unit tests to test each route individually.
const app = express()
app.use(helmet())
app.use(cors()) // TODO: Consider configuring CORS to be more restrictive, right now it allows all CORS requests.
app.use(logger('dev'))
app.use(express.json())
app.use(express.urlencoded({ extended: false }))
app.use(cookieParser())
app.use('/', indexRouter)
// Custom error handler.
app.use((err, req, res, next) => {
// As by expressjs documentation, when the headers have already
// been sent to the client, we must delegate to the default error handler.
if (res.headersSent) { return next(err) }
if (err instanceof HttpError) {
return res.status(err.statusCode).json({ message: err.message, data: err.data })
}
return next(err)
})
export default app

View File

@ -0,0 +1,22 @@
import _ from 'lodash'
const env = process.env.NODE_ENV || 'development'
// TODO:
// - Use dotenv library to consume env vars from a file.
// - Use convict library to define schema and validate env vars.
// https://codingsans.com/blog/node-config-best-practices
const config = {
all: {
env,
port: parseInt(process.env.PORT) || 3001,
},
development: {
},
production: {
}
}
const resolvedConfig = _.merge(config.all, config[env])
export default resolvedConfig

View File

@ -0,0 +1,17 @@
class AuthError extends Error {
constructor (message, data, ...params) {
super(message, ...params)
if (Error.captureStackTrace) {
Error.captureStackTrace(this, AuthError)
}
this.name = this.constructor.name
if (data) {
this.data = data
}
}
}
export default AuthError

View File

@ -0,0 +1,22 @@
class HttpError extends Error {
constructor (statusCode, message, data, ...params) {
super(message, ...params)
if (Error.captureStackTrace) {
Error.captureStackTrace(this, HttpError)
}
this.name = this.constructor.name
if (!(Number.isInteger(statusCode) && statusCode >= 400 && statusCode < 600)) {
throw new Error('statusCode has to be integer in range [400, 600).')
}
this.statusCode = statusCode
if (data) {
this.data = data
}
}
}
export default HttpError

View File

@ -0,0 +1,13 @@
import Prisma from '@prisma/client'
const createDbClient = () => {
const prismaClient = new Prisma.PrismaClient()
return prismaClient
}
const dbClient = createDbClient()
export default dbClient

View File

@ -0,0 +1,81 @@
* {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
box-sizing: border-box;
}
.container {
min-height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
main {
padding: 5rem 0;
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
main p {
font-size: 1.2rem;
}
.logo {
margin-bottom: 2rem;
}
.logo img {
max-height: 200px;
}
.welcome-title {
font-weight: 500;
}
.welcome-subtitle {
font-weight: 400;
margin-bottom: 3rem;
}
.buttons {
display: flex;
flex-direction: row;
}
.buttons .button:not(:last-child) {
margin-right: 0.5rem;
}
.button {
border-radius: 3px;
font-size: 1.2rem;
padding: 1rem 2rem;
text-align: center;
font-weight: 700;
text-decoration: none;
}
.button-filled {
border: 2px solid #bf9900;
background-color: #bf9900;
color: #f4f4f4;
}
.button-outline {
border: 2px solid #8a9cff;
color: #8a9cff;
background-color: none;
}
code {
border-radius: 5px;
padding: 0.2rem;
background: #efefef;
font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono,
Bitstream Vera Sans Mono, Courier New, monospace;
}

View File

@ -0,0 +1,41 @@
import React from 'react'
import waspLogo from './waspLogo.png'
import './Main.css'
const MainPage = () => {
return (
<div className="container">
<main>
<div className="logo">
<img src={waspLogo} alt="wasp" />
</div>
<h2 className="welcome-title"> Welcome to Wasp - you just started a new app! </h2>
<h3 className="welcome-subtitle">
This is page <code>MainPage</code> located at route <code>/</code>.
Open <code>ext/MainPage.js</code> to edit it.
</h3>
<div className="buttons">
<a
className="button button-filled"
href="https://wasp-lang.dev/docs/tutorials/todo-app"
target="_blank"
rel="noreferrer noopener"
>
Take the Tutorial
</a>
<a
className="button button-outline"
href="https://discord.com/invite/rzdnErX"
target="_blank"
rel="noreferrer noopener"
>
Chat on Discord
</a>
</div>
</main>
</div>
)
}
export default MainPage

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View File

@ -0,0 +1,13 @@
import express from 'express'
import operations from './operations/index.js'
const router = express.Router()
router.get('/', function (req, res, next) {
res.json('Hello world')
})
router.use('/operations', operations)
export default router

View File

@ -0,0 +1,8 @@
import express from 'express'
const router = express.Router()
export default router

View File

@ -0,0 +1,53 @@
import debug from 'debug'
import http from 'http'
import app from './app.js'
import config from './config.js'
const startServer = async () => {
const debugLog = debug('server:server')
const port = normalizePort(config.port)
app.set('port', port)
const server = http.createServer(app)
server.listen(port)
server.on('error', (error) => {
if (error.syscall !== 'listen') throw error
const bind = typeof port === 'string' ? 'Pipe ' + port : 'Port ' + port
// handle specific listen errors with friendly messages
switch (error.code) {
case 'EACCES':
console.error(bind + ' requires elevated privileges')
process.exit(1)
case 'EADDRINUSE':
console.error(bind + ' is already in use')
process.exit(1)
default:
throw error
}
})
server.on('listening', () => {
const addr = server.address()
const bind = typeof addr === 'string' ? 'pipe ' + addr : 'port ' + addr.port
debugLog('Listening on ' + bind)
})
}
startServer().catch(e => console.error(e))
/**
* Normalize a port into a number, string, or false.
*/
function normalizePort (val) {
const port = parseInt(val, 10)
if (isNaN(port)) return val // named pipe
if (port >= 0) return port // port number
return false
}

View File

@ -0,0 +1,45 @@
import Prisma from '@prisma/client'
import HttpError from './core/HttpError.js'
/**
* Decorator for async express middleware that handles promise rejections.
* @param {Func} middleware - Express middleware function.
* @returns {Func} Express middleware that is exactly the same as the given middleware but,
* if given middleware returns promise, reject of that promise will be correctly handled,
* meaning that error will be forwarded to next().
*/
export const handleRejection = (middleware) => async (req, res, next) => {
try {
await middleware(req, res, next)
} catch (error) {
next(error)
}
}
export const isPrismaError = (e) => {
return e instanceof Prisma.PrismaClientKnownRequestError ||
e instanceof Prisma.PrismaClientUnknownRequestError ||
e instanceof Prisma.PrismaClientRustPanicError ||
e instanceof Prisma.PrismaClientInitializationError ||
e instanceof Prisma.PrismaClientValidationError
}
export const prismaErrorToHttpError = (e) => {
if (e instanceof Prisma.PrismaClientKnownRequestError) {
if (e.code === 'P2002') {
return new HttpError(422, 'Save failed', {
message: `A record with the same ${e.meta.target.join(', ')} already exists.`,
target: e.meta.target
})
} else {
// TODO(shayne): Go through https://www.prisma.io/docs/reference/api-reference/error-reference#error-codes
// and decide which are input errors (422) and which are not (500)
// See: https://github.com/wasp-lang/wasp/issues/384
return new HttpError(500)
}
} else if (e instanceof Prisma.PrismaClientValidationError) {
return new HttpError(422, 'Save failed')
} else {
return new HttpError(500)
}
}

View File

@ -0,0 +1,68 @@
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
## Available Scripts
In the project directory, you can run:
### `npm start`
Runs the app in the development mode.<br>
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
The page will reload if you make edits.<br>
You will also see any lint errors in the console.
### `npm test`
Launches the test runner in the interactive watch mode.<br>
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
### `npm run build`
Builds the app for production to the `build` folder.<br>
It correctly bundles React in production mode and optimizes the build for the best performance.
The build is minified and the filenames include the hashes.<br>
Your app is ready to be deployed!
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
### `npm run eject`
**Note: this is a one-way operation. Once you `eject`, you cant go back!**
If you arent satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point youre on your own.
You dont have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldnt feel obligated to use this feature. However we understand that this tool wouldnt be useful if you couldnt customize it when you are ready for it.
## Learn More
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
To learn React, check out the [React documentation](https://reactjs.org/).
### Code Splitting
This section has moved here: https://facebook.github.io/create-react-app/docs/code-splitting
### Analyzing the Bundle Size
This section has moved here: https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size
### Making a Progressive Web App
This section has moved here: https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app
### Advanced Configuration
This section has moved here: https://facebook.github.io/create-react-app/docs/advanced-configuration
### Deployment
This section has moved here: https://facebook.github.io/create-react-app/docs/deployment
### `npm run build` fails to minify
This section has moved here: https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify

View File

@ -0,0 +1,8 @@
[build]
publish = "build/"
[[redirects]]
from = "/*"
to = "/index.html"
status = 200
force = false

View File

@ -0,0 +1,37 @@
{
"browserslist": {
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
],
"production": [
">0.2%",
"not dead",
"not op_mini all"
]
},
"dependencies": {
"axios": "^0.21.1",
"lodash": "^4.17.15",
"react": "^16.12.0",
"react-dom": "^16.12.0",
"react-query": "^2.14.1",
"react-router-dom": "^5.1.2",
"react-scripts": "4.0.3",
"uuid": "^3.4.0"
},
"devDependencies": {},
"eslintConfig": {
"extends": "react-app"
},
"name": "waspBuild",
"private": true,
"scripts": {
"build": "react-scripts build",
"eject": "react-scripts eject",
"start": "react-scripts start",
"test": "react-scripts test"
},
"version": "0.0.0"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -0,0 +1,49 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" />
<meta
name="viewport"
content="minimum-scale=1, initial-scale=1, width=device-width, shrink-to-fit=no"
/>
<meta name="theme-color" content="#000000" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>waspBuild</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

View File

@ -0,0 +1,14 @@
{
"name": "waspBuild",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

View File

@ -0,0 +1,74 @@
import axios from 'axios'
import config from './config'
const api = axios.create({
baseURL: config.apiUrl,
})
const WASP_APP_AUTH_TOKEN_NAME = "authToken"
let authToken = null
if (window.localStorage) {
authToken = window.localStorage.getItem(WASP_APP_AUTH_TOKEN_NAME)
}
export const setAuthToken = (token) => {
if (typeof token !== 'string') {
throw Error(`Token must be a string, but it was: {${typeof token}} ${token}.`)
}
authToken = token
window.localStorage && window.localStorage.setItem(WASP_APP_AUTH_TOKEN_NAME, token)
}
export const clearAuthToken = () => {
authToken = undefined
window.localStorage && window.localStorage.removeItem(WASP_APP_AUTH_TOKEN_NAME)
}
export const clearLocalStorage = () => {
authToken = undefined
window.localStorage && window.localStorage.clear()
}
api.interceptors.request.use(request => {
if (authToken) {
request.headers['Authorization'] = `Bearer ${authToken}`
}
return request
})
api.interceptors.response.use(undefined, error => {
if (error.response?.status === 401) {
clearAuthToken()
}
return Promise.reject(error)
})
/**
* Takes an error returned by the app's API (as returned by axios), and transforms into a more
* standard format to be further used by the client. It is also assumed that given API
* error has been formatted as implemented by HttpError on the server.
*/
export const handleApiError = (error) => {
if (error?.response) {
// If error came from HTTP response, we capture most informative message
// and also add .statusCode information to it.
// If error had JSON response, we assume it is of format { message, data } and
// add that info to the error.
// TODO: We might want to use HttpError here instead of just Error, since
// HttpError is also used on server to throw errors like these.
// That would require copying HttpError code to web-app also and using it here.
const responseJson = error.response?.data
const responseStatusCode = error.response.status
const e = new Error(responseJson?.message || error.message)
e.statusCode = responseStatusCode
e.data = responseJson?.data
throw e
} else {
// If any other error, we just propagate it.
throw error
}
}
export default api

View File

@ -0,0 +1,6 @@
const config = {
apiUrl: process.env.REACT_APP_API_URL || 'http://localhost:3001'
}
export default config

View File

@ -0,0 +1,81 @@
* {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
box-sizing: border-box;
}
.container {
min-height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
main {
padding: 5rem 0;
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
main p {
font-size: 1.2rem;
}
.logo {
margin-bottom: 2rem;
}
.logo img {
max-height: 200px;
}
.welcome-title {
font-weight: 500;
}
.welcome-subtitle {
font-weight: 400;
margin-bottom: 3rem;
}
.buttons {
display: flex;
flex-direction: row;
}
.buttons .button:not(:last-child) {
margin-right: 0.5rem;
}
.button {
border-radius: 3px;
font-size: 1.2rem;
padding: 1rem 2rem;
text-align: center;
font-weight: 700;
text-decoration: none;
}
.button-filled {
border: 2px solid #bf9900;
background-color: #bf9900;
color: #f4f4f4;
}
.button-outline {
border: 2px solid #8a9cff;
color: #8a9cff;
background-color: none;
}
code {
border-radius: 5px;
padding: 0.2rem;
background: #efefef;
font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono,
Bitstream Vera Sans Mono, Courier New, monospace;
}

View File

@ -0,0 +1,41 @@
import React from 'react'
import waspLogo from './waspLogo.png'
import './Main.css'
const MainPage = () => {
return (
<div className="container">
<main>
<div className="logo">
<img src={waspLogo} alt="wasp" />
</div>
<h2 className="welcome-title"> Welcome to Wasp - you just started a new app! </h2>
<h3 className="welcome-subtitle">
This is page <code>MainPage</code> located at route <code>/</code>.
Open <code>ext/MainPage.js</code> to edit it.
</h3>
<div className="buttons">
<a
className="button button-filled"
href="https://wasp-lang.dev/docs/tutorials/todo-app"
target="_blank"
rel="noreferrer noopener"
>
Take the Tutorial
</a>
<a
className="button button-outline"
href="https://discord.com/invite/rzdnErX"
target="_blank"
rel="noreferrer noopener"
>
Chat on Discord
</a>
</div>
</main>
</div>
)
}
export default MainPage

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View File

@ -0,0 +1,14 @@
body {
margin: 0;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
monospace;
}

View File

@ -0,0 +1,22 @@
import React from 'react'
import ReactDOM from 'react-dom'
import { ReactQueryCacheProvider } from 'react-query'
import router from './router'
import queryCache from './queryCache'
import * as serviceWorker from './serviceWorker'
import './index.css'
ReactDOM.render(
<ReactQueryCacheProvider queryCache={queryCache}>
{ router }
</ReactQueryCacheProvider>,
document.getElementById('root')
)
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();

Binary file not shown.

After

Width:  |  Height:  |  Size: 818 B

View File

@ -0,0 +1,12 @@
import api, { handleApiError } from '../api.js'
import config from '../config.js'
export const callOperation = async (operationRoute, args) => {
try {
const response = await api.post(config.apiUrl + '/' + operationRoute, args)
return response.data
} catch (error) {
handleApiError(error)
}
}

View File

@ -0,0 +1,46 @@
import queryCache from '../queryCache'
// Map where key is resource name and value is Set
// containing query ids of all the queries that use
// that resource.
const resourceToQueryCacheKeys = new Map()
/**
* Remembers that specified query is using specified resources.
* If called multiple times for same query, resources are added, not reset.
* @param {string} queryCacheKey - Unique key under used to identify query in the cache.
* @param {string[]} resources - Names of resources that query is using.
*/
export const addResourcesUsedByQuery = (queryCacheKey, resources) => {
for (const resource of resources) {
let cacheKeys = resourceToQueryCacheKeys.get(resource)
if (!cacheKeys) {
cacheKeys = new Set()
resourceToQueryCacheKeys.set(resource, cacheKeys)
}
cacheKeys.add(queryCacheKey)
}
}
/**
* @param {string} resource - Resource name.
* @returns {string[]} Array of "query cache keys" of queries that use specified resource.
*/
export const getQueriesUsingResource = (resource) => {
return Array.from(resourceToQueryCacheKeys.get(resource) || [])
}
/**
* Invalidates all queries that are using specified resources.
* @param {string[]} resources - Names of resources.
*/
export const invalidateQueriesUsing = (resources) => {
const queryCacheKeysToInvalidate = new Set()
for (const resource of resources) {
getQueriesUsingResource(resource).forEach(key => queryCacheKeysToInvalidate.add(key))
}
for (const queryCacheKey of queryCacheKeysToInvalidate) {
queryCache.invalidateQueries(queryCacheKey)
}
}

View File

@ -0,0 +1,18 @@
import { useQuery as rqUseQuery } from 'react-query'
export const useQuery = (queryFn, queryFnArgs, config) => {
if (typeof queryFn !== 'function') {
throw new Error('useQuery requires queryFn to be a function.')
}
if (!queryFn.queryCacheKey) {
throw new Error('queryFn needs to have queryCacheKey property defined.')
}
const rqResult = rqUseQuery({
queryKey: [queryFn.queryCacheKey, queryFnArgs],
queryFn: (_key, args) => queryFn(args),
config
})
return rqResult
}

View File

@ -0,0 +1,3 @@
import { QueryCache } from 'react-query'
export default new QueryCache()

View File

@ -0,0 +1,16 @@
import React from 'react'
import { Route, BrowserRouter as Router } from 'react-router-dom'
import MainPage from "./ext-src/MainPage.js"
const router = (
<Router>
<div>
<Route exact path="/" component={ MainPage }/>
</div>
</Router>
)
export default router

View File

@ -0,0 +1,135 @@
// This optional code is used to register a service worker.
// register() is not called by default.
// This lets the app load faster on subsequent visits in production, and gives
// it offline capabilities. However, it also means that developers (and users)
// will only see deployed updates on subsequent visits to a page, after all the
// existing tabs open on the page have been closed, since previously cached
// resources are updated in the background.
// To learn more about the benefits of this model and instructions on how to
// opt-in, read https://bit.ly/CRA-PWA
const isLocalhost = Boolean(
window.location.hostname === 'localhost' ||
// [::1] is the IPv6 localhost address.
window.location.hostname === '[::1]' ||
// 127.0.0.1/8 is considered localhost for IPv4.
window.location.hostname.match(
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
)
);
export function register(config) {
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
// The URL constructor is available in all browsers that support SW.
const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
if (publicUrl.origin !== window.location.origin) {
// Our service worker won't work if PUBLIC_URL is on a different origin
// from what our page is served on. This might happen if a CDN is used to
// serve assets; see https://github.com/facebook/create-react-app/issues/2374
return;
}
window.addEventListener('load', () => {
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
if (isLocalhost) {
// This is running on localhost. Let's check if a service worker still exists or not.
checkValidServiceWorker(swUrl, config);
// Add some additional logging to localhost, pointing developers to the
// service worker/PWA documentation.
navigator.serviceWorker.ready.then(() => {
console.log(
'This web app is being served cache-first by a service ' +
'worker. To learn more, visit https://bit.ly/CRA-PWA'
);
});
} else {
// Is not localhost. Just register service worker
registerValidSW(swUrl, config);
}
});
}
}
function registerValidSW(swUrl, config) {
navigator.serviceWorker
.register(swUrl)
.then(registration => {
registration.onupdatefound = () => {
const installingWorker = registration.installing;
if (installingWorker == null) {
return;
}
installingWorker.onstatechange = () => {
if (installingWorker.state === 'installed') {
if (navigator.serviceWorker.controller) {
// At this point, the updated precached content has been fetched,
// but the previous service worker will still serve the older
// content until all client tabs are closed.
console.log(
'New content is available and will be used when all ' +
'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
);
// Execute callback
if (config && config.onUpdate) {
config.onUpdate(registration);
}
} else {
// At this point, everything has been precached.
// It's the perfect time to display a
// "Content is cached for offline use." message.
console.log('Content is cached for offline use.');
// Execute callback
if (config && config.onSuccess) {
config.onSuccess(registration);
}
}
}
};
};
})
.catch(error => {
console.error('Error during service worker registration:', error);
});
}
function checkValidServiceWorker(swUrl, config) {
// Check if the service worker can be found. If it can't reload the page.
fetch(swUrl)
.then(response => {
// Ensure service worker exists, and that we really are getting a JS file.
const contentType = response.headers.get('content-type');
if (
response.status === 404 ||
(contentType != null && contentType.indexOf('javascript') === -1)
) {
// No service worker found. Probably a different app. Reload the page.
navigator.serviceWorker.ready.then(registration => {
registration.unregister().then(() => {
window.location.reload();
});
});
} else {
// Service worker found. Proceed as normal.
registerValidSW(swUrl, config);
}
})
.catch(() => {
console.log(
'No internet connection found. App is running in offline mode.'
);
});
}
export function unregister() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.ready.then(registration => {
registration.unregister();
});
}
}

View File

@ -0,0 +1,3 @@
export const errorMessage = (e) => {
return `Error: ${e.message} ${e.data?.message ? '- Details: ' + e.data.message : ''}`
}

View File

@ -0,0 +1 @@
File marking the root of Wasp project.

View File

@ -0,0 +1,3 @@
# Ignore editor tmp files
**/*~
**/#*#

View File

@ -0,0 +1,81 @@
* {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
box-sizing: border-box;
}
.container {
min-height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
main {
padding: 5rem 0;
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
main p {
font-size: 1.2rem;
}
.logo {
margin-bottom: 2rem;
}
.logo img {
max-height: 200px;
}
.welcome-title {
font-weight: 500;
}
.welcome-subtitle {
font-weight: 400;
margin-bottom: 3rem;
}
.buttons {
display: flex;
flex-direction: row;
}
.buttons .button:not(:last-child) {
margin-right: 0.5rem;
}
.button {
border-radius: 3px;
font-size: 1.2rem;
padding: 1rem 2rem;
text-align: center;
font-weight: 700;
text-decoration: none;
}
.button-filled {
border: 2px solid #bf9900;
background-color: #bf9900;
color: #f4f4f4;
}
.button-outline {
border: 2px solid #8a9cff;
color: #8a9cff;
background-color: none;
}
code {
border-radius: 5px;
padding: 0.2rem;
background: #efefef;
font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono,
Bitstream Vera Sans Mono, Courier New, monospace;
}

View File

@ -0,0 +1,41 @@
import React from 'react'
import waspLogo from './waspLogo.png'
import './Main.css'
const MainPage = () => {
return (
<div className="container">
<main>
<div className="logo">
<img src={waspLogo} alt="wasp" />
</div>
<h2 className="welcome-title"> Welcome to Wasp - you just started a new app! </h2>
<h3 className="welcome-subtitle">
This is page <code>MainPage</code> located at route <code>/</code>.
Open <code>ext/MainPage.js</code> to edit it.
</h3>
<div className="buttons">
<a
className="button button-filled"
href="https://wasp-lang.dev/docs/tutorials/todo-app"
target="_blank"
rel="noreferrer noopener"
>
Take the Tutorial
</a>
<a
className="button button-outline"
href="https://discord.com/invite/rzdnErX"
target="_blank"
rel="noreferrer noopener"
>
Chat on Discord
</a>
</div>
</main>
</div>
)
}
export default MainPage

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View File

@ -0,0 +1,9 @@
app waspBuild {
db: { system: PostgreSQL },
title: "waspBuild"
}
route RootRoute { path: "/", to: MainPage }
page MainPage {
component: import Main from "@ext/MainPage.js"
}

View File

@ -0,0 +1,47 @@
waspCompile/.wasp/out/.dockerignore
waspCompile/.wasp/out/Dockerfile
waspCompile/.wasp/out/db/schema.prisma
waspCompile/.wasp/out/installedFullStackNpmDependencies.json
waspCompile/.wasp/out/server/.npmrc
waspCompile/.wasp/out/server/.nvmrc
waspCompile/.wasp/out/server/README.md
waspCompile/.wasp/out/server/package.json
waspCompile/.wasp/out/server/src/app.js
waspCompile/.wasp/out/server/src/config.js
waspCompile/.wasp/out/server/src/core/AuthError.js
waspCompile/.wasp/out/server/src/core/HttpError.js
waspCompile/.wasp/out/server/src/dbClient.js
waspCompile/.wasp/out/server/src/ext-src/Main.css
waspCompile/.wasp/out/server/src/ext-src/MainPage.js
waspCompile/.wasp/out/server/src/ext-src/waspLogo.png
waspCompile/.wasp/out/server/src/routes/index.js
waspCompile/.wasp/out/server/src/routes/operations/index.js
waspCompile/.wasp/out/server/src/server.js
waspCompile/.wasp/out/server/src/utils.js
waspCompile/.wasp/out/web-app/README.md
waspCompile/.wasp/out/web-app/netlify.toml
waspCompile/.wasp/out/web-app/package.json
waspCompile/.wasp/out/web-app/public/favicon.ico
waspCompile/.wasp/out/web-app/public/index.html
waspCompile/.wasp/out/web-app/public/manifest.json
waspCompile/.wasp/out/web-app/src/api.js
waspCompile/.wasp/out/web-app/src/config.js
waspCompile/.wasp/out/web-app/src/ext-src/Main.css
waspCompile/.wasp/out/web-app/src/ext-src/MainPage.js
waspCompile/.wasp/out/web-app/src/ext-src/waspLogo.png
waspCompile/.wasp/out/web-app/src/index.css
waspCompile/.wasp/out/web-app/src/index.js
waspCompile/.wasp/out/web-app/src/logo.png
waspCompile/.wasp/out/web-app/src/operations/index.js
waspCompile/.wasp/out/web-app/src/operations/resources.js
waspCompile/.wasp/out/web-app/src/queries/index.js
waspCompile/.wasp/out/web-app/src/queryCache.js
waspCompile/.wasp/out/web-app/src/router.js
waspCompile/.wasp/out/web-app/src/serviceWorker.js
waspCompile/.wasp/out/web-app/src/utils.js
waspCompile/.wasproot
waspCompile/ext/.waspignore
waspCompile/ext/Main.css
waspCompile/ext/MainPage.js
waspCompile/ext/waspLogo.png
waspCompile/main.wasp

View File

@ -0,0 +1,2 @@
**/node_modules/
**/.git

View File

@ -0,0 +1,27 @@
FROM node:14-alpine AS node
FROM node AS base
RUN apk --no-cache -U upgrade # To ensure any potential security patches are applied.
FROM base AS server-builder
# Install packages needed to build native npm packages.
RUN apk add --no-cache build-base libtool autoconf automake python
WORKDIR /app
# Install npm packages, resulting in node_modules/.
COPY server/package*.json ./server/
RUN cd server && npm install
# TODO: Use pm2?
# TODO: Use non-root user (node).
FROM base AS server-production
ENV NODE_ENV production
WORKDIR /app
COPY --from=server-builder /app/server/node_modules ./server/node_modules
COPY server/ ./server/
COPY db/ ./db/
EXPOSE ${PORT}
WORKDIR /app/server
ENTRYPOINT ["npm", "run", "start-production"]

View File

@ -0,0 +1,11 @@
datasource db {
provider = "sqlite"
url = "file:./dev.db"
}
generator client {
provider = "prisma-client-js"
output = "../server/node_modules/.prisma/client"
}

View File

@ -0,0 +1 @@
{"npmDepsForServer":{"dependencies":[{"name":"cookie-parser","version":"~1.4.4"},{"name":"cors","version":"^2.8.5"},{"name":"debug","version":"~2.6.9"},{"name":"express","version":"~4.16.1"},{"name":"morgan","version":"~1.9.1"},{"name":"@prisma/client","version":"3.9.1"},{"name":"jsonwebtoken","version":"^8.5.1"},{"name":"secure-password","version":"^4.0.0"},{"name":"dotenv","version":"8.2.0"},{"name":"helmet","version":"^4.6.0"}],"devDependencies":[{"name":"nodemon","version":"^2.0.4"},{"name":"standard","version":"^14.3.4"},{"name":"prisma","version":"3.9.1"}]},"npmDepsForWebApp":{"dependencies":[{"name":"axios","version":"^0.21.1"},{"name":"lodash","version":"^4.17.15"},{"name":"react","version":"^16.12.0"},{"name":"react-dom","version":"^16.12.0"},{"name":"react-query","version":"^2.14.1"},{"name":"react-router-dom","version":"^5.1.2"},{"name":"react-scripts","version":"4.0.3"},{"name":"uuid","version":"^3.4.0"}],"devDependencies":[]}}

View File

@ -0,0 +1 @@
engine-strict=true

View File

@ -0,0 +1,5 @@
`npm start` to run server in development mode (it reloads on changes).
`npm run debug` to run `npm start` with debug logs enabled.
`npm run standard` to run StandardJS.

View File

@ -0,0 +1,37 @@
{
"dependencies": {
"@prisma/client": "3.9.1",
"cookie-parser": "~1.4.4",
"cors": "^2.8.5",
"debug": "~2.6.9",
"dotenv": "8.2.0",
"express": "~4.16.1",
"helmet": "^4.6.0",
"jsonwebtoken": "^8.5.1",
"morgan": "~1.9.1",
"secure-password": "^4.0.0"
},
"devDependencies": {
"nodemon": "^2.0.4",
"prisma": "3.9.1",
"standard": "^14.3.4"
},
"engines": {
"node": ">=12.18.0"
},
"name": "server",
"nodemonConfig": {
"delay": "1000"
},
"private": true,
"scripts": {
"db-migrate-dev": "prisma migrate dev --schema=../db/schema.prisma",
"db-migrate-prod": "prisma migrate deploy --schema=../db/schema.prisma",
"debug": "DEBUG=server:* npm start",
"standard": "standard",
"start": "nodemon -r dotenv/config ./src/server.js",
"start-production": "NODE_ENV=production node ./src/server.js"
},
"type": "module",
"version": "0.0.0"
}

View File

@ -0,0 +1,37 @@
import express from 'express'
import cookieParser from 'cookie-parser'
import logger from 'morgan'
import cors from 'cors'
import helmet from 'helmet'
import HttpError from './core/HttpError.js'
import indexRouter from './routes/index.js'
// TODO: Consider extracting most of this logic into createApp(routes, path) function so that
// it can be used in unit tests to test each route individually.
const app = express()
app.use(helmet())
app.use(cors()) // TODO: Consider configuring CORS to be more restrictive, right now it allows all CORS requests.
app.use(logger('dev'))
app.use(express.json())
app.use(express.urlencoded({ extended: false }))
app.use(cookieParser())
app.use('/', indexRouter)
// Custom error handler.
app.use((err, req, res, next) => {
// As by expressjs documentation, when the headers have already
// been sent to the client, we must delegate to the default error handler.
if (res.headersSent) { return next(err) }
if (err instanceof HttpError) {
return res.status(err.statusCode).json({ message: err.message, data: err.data })
}
return next(err)
})
export default app

View File

@ -0,0 +1,22 @@
import _ from 'lodash'
const env = process.env.NODE_ENV || 'development'
// TODO:
// - Use dotenv library to consume env vars from a file.
// - Use convict library to define schema and validate env vars.
// https://codingsans.com/blog/node-config-best-practices
const config = {
all: {
env,
port: parseInt(process.env.PORT) || 3001,
},
development: {
},
production: {
}
}
const resolvedConfig = _.merge(config.all, config[env])
export default resolvedConfig

View File

@ -0,0 +1,17 @@
class AuthError extends Error {
constructor (message, data, ...params) {
super(message, ...params)
if (Error.captureStackTrace) {
Error.captureStackTrace(this, AuthError)
}
this.name = this.constructor.name
if (data) {
this.data = data
}
}
}
export default AuthError

View File

@ -0,0 +1,22 @@
class HttpError extends Error {
constructor (statusCode, message, data, ...params) {
super(message, ...params)
if (Error.captureStackTrace) {
Error.captureStackTrace(this, HttpError)
}
this.name = this.constructor.name
if (!(Number.isInteger(statusCode) && statusCode >= 400 && statusCode < 600)) {
throw new Error('statusCode has to be integer in range [400, 600).')
}
this.statusCode = statusCode
if (data) {
this.data = data
}
}
}
export default HttpError

View File

@ -0,0 +1,13 @@
import Prisma from '@prisma/client'
const createDbClient = () => {
const prismaClient = new Prisma.PrismaClient()
return prismaClient
}
const dbClient = createDbClient()
export default dbClient

View File

@ -0,0 +1,81 @@
* {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
box-sizing: border-box;
}
.container {
min-height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
main {
padding: 5rem 0;
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
main p {
font-size: 1.2rem;
}
.logo {
margin-bottom: 2rem;
}
.logo img {
max-height: 200px;
}
.welcome-title {
font-weight: 500;
}
.welcome-subtitle {
font-weight: 400;
margin-bottom: 3rem;
}
.buttons {
display: flex;
flex-direction: row;
}
.buttons .button:not(:last-child) {
margin-right: 0.5rem;
}
.button {
border-radius: 3px;
font-size: 1.2rem;
padding: 1rem 2rem;
text-align: center;
font-weight: 700;
text-decoration: none;
}
.button-filled {
border: 2px solid #bf9900;
background-color: #bf9900;
color: #f4f4f4;
}
.button-outline {
border: 2px solid #8a9cff;
color: #8a9cff;
background-color: none;
}
code {
border-radius: 5px;
padding: 0.2rem;
background: #efefef;
font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono,
Bitstream Vera Sans Mono, Courier New, monospace;
}

View File

@ -0,0 +1,41 @@
import React from 'react'
import waspLogo from './waspLogo.png'
import './Main.css'
const MainPage = () => {
return (
<div className="container">
<main>
<div className="logo">
<img src={waspLogo} alt="wasp" />
</div>
<h2 className="welcome-title"> Welcome to Wasp - you just started a new app! </h2>
<h3 className="welcome-subtitle">
This is page <code>MainPage</code> located at route <code>/</code>.
Open <code>ext/MainPage.js</code> to edit it.
</h3>
<div className="buttons">
<a
className="button button-filled"
href="https://wasp-lang.dev/docs/tutorials/todo-app"
target="_blank"
rel="noreferrer noopener"
>
Take the Tutorial
</a>
<a
className="button button-outline"
href="https://discord.com/invite/rzdnErX"
target="_blank"
rel="noreferrer noopener"
>
Chat on Discord
</a>
</div>
</main>
</div>
)
}
export default MainPage

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View File

@ -0,0 +1,13 @@
import express from 'express'
import operations from './operations/index.js'
const router = express.Router()
router.get('/', function (req, res, next) {
res.json('Hello world')
})
router.use('/operations', operations)
export default router

View File

@ -0,0 +1,8 @@
import express from 'express'
const router = express.Router()
export default router

View File

@ -0,0 +1,53 @@
import debug from 'debug'
import http from 'http'
import app from './app.js'
import config from './config.js'
const startServer = async () => {
const debugLog = debug('server:server')
const port = normalizePort(config.port)
app.set('port', port)
const server = http.createServer(app)
server.listen(port)
server.on('error', (error) => {
if (error.syscall !== 'listen') throw error
const bind = typeof port === 'string' ? 'Pipe ' + port : 'Port ' + port
// handle specific listen errors with friendly messages
switch (error.code) {
case 'EACCES':
console.error(bind + ' requires elevated privileges')
process.exit(1)
case 'EADDRINUSE':
console.error(bind + ' is already in use')
process.exit(1)
default:
throw error
}
})
server.on('listening', () => {
const addr = server.address()
const bind = typeof addr === 'string' ? 'pipe ' + addr : 'port ' + addr.port
debugLog('Listening on ' + bind)
})
}
startServer().catch(e => console.error(e))
/**
* Normalize a port into a number, string, or false.
*/
function normalizePort (val) {
const port = parseInt(val, 10)
if (isNaN(port)) return val // named pipe
if (port >= 0) return port // port number
return false
}

View File

@ -0,0 +1,45 @@
import Prisma from '@prisma/client'
import HttpError from './core/HttpError.js'
/**
* Decorator for async express middleware that handles promise rejections.
* @param {Func} middleware - Express middleware function.
* @returns {Func} Express middleware that is exactly the same as the given middleware but,
* if given middleware returns promise, reject of that promise will be correctly handled,
* meaning that error will be forwarded to next().
*/
export const handleRejection = (middleware) => async (req, res, next) => {
try {
await middleware(req, res, next)
} catch (error) {
next(error)
}
}
export const isPrismaError = (e) => {
return e instanceof Prisma.PrismaClientKnownRequestError ||
e instanceof Prisma.PrismaClientUnknownRequestError ||
e instanceof Prisma.PrismaClientRustPanicError ||
e instanceof Prisma.PrismaClientInitializationError ||
e instanceof Prisma.PrismaClientValidationError
}
export const prismaErrorToHttpError = (e) => {
if (e instanceof Prisma.PrismaClientKnownRequestError) {
if (e.code === 'P2002') {
return new HttpError(422, 'Save failed', {
message: `A record with the same ${e.meta.target.join(', ')} already exists.`,
target: e.meta.target
})
} else {
// TODO(shayne): Go through https://www.prisma.io/docs/reference/api-reference/error-reference#error-codes
// and decide which are input errors (422) and which are not (500)
// See: https://github.com/wasp-lang/wasp/issues/384
return new HttpError(500)
}
} else if (e instanceof Prisma.PrismaClientValidationError) {
return new HttpError(422, 'Save failed')
} else {
return new HttpError(500)
}
}

View File

@ -0,0 +1,68 @@
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
## Available Scripts
In the project directory, you can run:
### `npm start`
Runs the app in the development mode.<br>
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
The page will reload if you make edits.<br>
You will also see any lint errors in the console.
### `npm test`
Launches the test runner in the interactive watch mode.<br>
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
### `npm run build`
Builds the app for production to the `build` folder.<br>
It correctly bundles React in production mode and optimizes the build for the best performance.
The build is minified and the filenames include the hashes.<br>
Your app is ready to be deployed!
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
### `npm run eject`
**Note: this is a one-way operation. Once you `eject`, you cant go back!**
If you arent satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point youre on your own.
You dont have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldnt feel obligated to use this feature. However we understand that this tool wouldnt be useful if you couldnt customize it when you are ready for it.
## Learn More
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
To learn React, check out the [React documentation](https://reactjs.org/).
### Code Splitting
This section has moved here: https://facebook.github.io/create-react-app/docs/code-splitting
### Analyzing the Bundle Size
This section has moved here: https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size
### Making a Progressive Web App
This section has moved here: https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app
### Advanced Configuration
This section has moved here: https://facebook.github.io/create-react-app/docs/advanced-configuration
### Deployment
This section has moved here: https://facebook.github.io/create-react-app/docs/deployment
### `npm run build` fails to minify
This section has moved here: https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify

View File

@ -0,0 +1,8 @@
[build]
publish = "build/"
[[redirects]]
from = "/*"
to = "/index.html"
status = 200
force = false

View File

@ -0,0 +1,37 @@
{
"browserslist": {
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
],
"production": [
">0.2%",
"not dead",
"not op_mini all"
]
},
"dependencies": {
"axios": "^0.21.1",
"lodash": "^4.17.15",
"react": "^16.12.0",
"react-dom": "^16.12.0",
"react-query": "^2.14.1",
"react-router-dom": "^5.1.2",
"react-scripts": "4.0.3",
"uuid": "^3.4.0"
},
"devDependencies": {},
"eslintConfig": {
"extends": "react-app"
},
"name": "waspCompile",
"private": true,
"scripts": {
"build": "react-scripts build",
"eject": "react-scripts eject",
"start": "react-scripts start",
"test": "react-scripts test"
},
"version": "0.0.0"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -0,0 +1,49 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" />
<meta
name="viewport"
content="minimum-scale=1, initial-scale=1, width=device-width, shrink-to-fit=no"
/>
<meta name="theme-color" content="#000000" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>waspCompile</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

View File

@ -0,0 +1,14 @@
{
"name": "waspCompile",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

View File

@ -0,0 +1,74 @@
import axios from 'axios'
import config from './config'
const api = axios.create({
baseURL: config.apiUrl,
})
const WASP_APP_AUTH_TOKEN_NAME = "authToken"
let authToken = null
if (window.localStorage) {
authToken = window.localStorage.getItem(WASP_APP_AUTH_TOKEN_NAME)
}
export const setAuthToken = (token) => {
if (typeof token !== 'string') {
throw Error(`Token must be a string, but it was: {${typeof token}} ${token}.`)
}
authToken = token
window.localStorage && window.localStorage.setItem(WASP_APP_AUTH_TOKEN_NAME, token)
}
export const clearAuthToken = () => {
authToken = undefined
window.localStorage && window.localStorage.removeItem(WASP_APP_AUTH_TOKEN_NAME)
}
export const clearLocalStorage = () => {
authToken = undefined
window.localStorage && window.localStorage.clear()
}
api.interceptors.request.use(request => {
if (authToken) {
request.headers['Authorization'] = `Bearer ${authToken}`
}
return request
})
api.interceptors.response.use(undefined, error => {
if (error.response?.status === 401) {
clearAuthToken()
}
return Promise.reject(error)
})
/**
* Takes an error returned by the app's API (as returned by axios), and transforms into a more
* standard format to be further used by the client. It is also assumed that given API
* error has been formatted as implemented by HttpError on the server.
*/
export const handleApiError = (error) => {
if (error?.response) {
// If error came from HTTP response, we capture most informative message
// and also add .statusCode information to it.
// If error had JSON response, we assume it is of format { message, data } and
// add that info to the error.
// TODO: We might want to use HttpError here instead of just Error, since
// HttpError is also used on server to throw errors like these.
// That would require copying HttpError code to web-app also and using it here.
const responseJson = error.response?.data
const responseStatusCode = error.response.status
const e = new Error(responseJson?.message || error.message)
e.statusCode = responseStatusCode
e.data = responseJson?.data
throw e
} else {
// If any other error, we just propagate it.
throw error
}
}
export default api

View File

@ -0,0 +1,6 @@
const config = {
apiUrl: process.env.REACT_APP_API_URL || 'http://localhost:3001'
}
export default config

View File

@ -0,0 +1,81 @@
* {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
box-sizing: border-box;
}
.container {
min-height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
main {
padding: 5rem 0;
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
main p {
font-size: 1.2rem;
}
.logo {
margin-bottom: 2rem;
}
.logo img {
max-height: 200px;
}
.welcome-title {
font-weight: 500;
}
.welcome-subtitle {
font-weight: 400;
margin-bottom: 3rem;
}
.buttons {
display: flex;
flex-direction: row;
}
.buttons .button:not(:last-child) {
margin-right: 0.5rem;
}
.button {
border-radius: 3px;
font-size: 1.2rem;
padding: 1rem 2rem;
text-align: center;
font-weight: 700;
text-decoration: none;
}
.button-filled {
border: 2px solid #bf9900;
background-color: #bf9900;
color: #f4f4f4;
}
.button-outline {
border: 2px solid #8a9cff;
color: #8a9cff;
background-color: none;
}
code {
border-radius: 5px;
padding: 0.2rem;
background: #efefef;
font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono,
Bitstream Vera Sans Mono, Courier New, monospace;
}

View File

@ -0,0 +1,41 @@
import React from 'react'
import waspLogo from './waspLogo.png'
import './Main.css'
const MainPage = () => {
return (
<div className="container">
<main>
<div className="logo">
<img src={waspLogo} alt="wasp" />
</div>
<h2 className="welcome-title"> Welcome to Wasp - you just started a new app! </h2>
<h3 className="welcome-subtitle">
This is page <code>MainPage</code> located at route <code>/</code>.
Open <code>ext/MainPage.js</code> to edit it.
</h3>
<div className="buttons">
<a
className="button button-filled"
href="https://wasp-lang.dev/docs/tutorials/todo-app"
target="_blank"
rel="noreferrer noopener"
>
Take the Tutorial
</a>
<a
className="button button-outline"
href="https://discord.com/invite/rzdnErX"
target="_blank"
rel="noreferrer noopener"
>
Chat on Discord
</a>
</div>
</main>
</div>
)
}
export default MainPage

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View File

@ -0,0 +1,14 @@
body {
margin: 0;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
monospace;
}

View File

@ -0,0 +1,22 @@
import React from 'react'
import ReactDOM from 'react-dom'
import { ReactQueryCacheProvider } from 'react-query'
import router from './router'
import queryCache from './queryCache'
import * as serviceWorker from './serviceWorker'
import './index.css'
ReactDOM.render(
<ReactQueryCacheProvider queryCache={queryCache}>
{ router }
</ReactQueryCacheProvider>,
document.getElementById('root')
)
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();

Binary file not shown.

After

Width:  |  Height:  |  Size: 818 B

View File

@ -0,0 +1,12 @@
import api, { handleApiError } from '../api.js'
import config from '../config.js'
export const callOperation = async (operationRoute, args) => {
try {
const response = await api.post(config.apiUrl + '/' + operationRoute, args)
return response.data
} catch (error) {
handleApiError(error)
}
}

View File

@ -0,0 +1,46 @@
import queryCache from '../queryCache'
// Map where key is resource name and value is Set
// containing query ids of all the queries that use
// that resource.
const resourceToQueryCacheKeys = new Map()
/**
* Remembers that specified query is using specified resources.
* If called multiple times for same query, resources are added, not reset.
* @param {string} queryCacheKey - Unique key under used to identify query in the cache.
* @param {string[]} resources - Names of resources that query is using.
*/
export const addResourcesUsedByQuery = (queryCacheKey, resources) => {
for (const resource of resources) {
let cacheKeys = resourceToQueryCacheKeys.get(resource)
if (!cacheKeys) {
cacheKeys = new Set()
resourceToQueryCacheKeys.set(resource, cacheKeys)
}
cacheKeys.add(queryCacheKey)
}
}
/**
* @param {string} resource - Resource name.
* @returns {string[]} Array of "query cache keys" of queries that use specified resource.
*/
export const getQueriesUsingResource = (resource) => {
return Array.from(resourceToQueryCacheKeys.get(resource) || [])
}
/**
* Invalidates all queries that are using specified resources.
* @param {string[]} resources - Names of resources.
*/
export const invalidateQueriesUsing = (resources) => {
const queryCacheKeysToInvalidate = new Set()
for (const resource of resources) {
getQueriesUsingResource(resource).forEach(key => queryCacheKeysToInvalidate.add(key))
}
for (const queryCacheKey of queryCacheKeysToInvalidate) {
queryCache.invalidateQueries(queryCacheKey)
}
}

View File

@ -0,0 +1,18 @@
import { useQuery as rqUseQuery } from 'react-query'
export const useQuery = (queryFn, queryFnArgs, config) => {
if (typeof queryFn !== 'function') {
throw new Error('useQuery requires queryFn to be a function.')
}
if (!queryFn.queryCacheKey) {
throw new Error('queryFn needs to have queryCacheKey property defined.')
}
const rqResult = rqUseQuery({
queryKey: [queryFn.queryCacheKey, queryFnArgs],
queryFn: (_key, args) => queryFn(args),
config
})
return rqResult
}

View File

@ -0,0 +1,3 @@
import { QueryCache } from 'react-query'
export default new QueryCache()

View File

@ -0,0 +1,16 @@
import React from 'react'
import { Route, BrowserRouter as Router } from 'react-router-dom'
import MainPage from "./ext-src/MainPage.js"
const router = (
<Router>
<div>
<Route exact path="/" component={ MainPage }/>
</div>
</Router>
)
export default router

View File

@ -0,0 +1,135 @@
// This optional code is used to register a service worker.
// register() is not called by default.
// This lets the app load faster on subsequent visits in production, and gives
// it offline capabilities. However, it also means that developers (and users)
// will only see deployed updates on subsequent visits to a page, after all the
// existing tabs open on the page have been closed, since previously cached
// resources are updated in the background.
// To learn more about the benefits of this model and instructions on how to
// opt-in, read https://bit.ly/CRA-PWA
const isLocalhost = Boolean(
window.location.hostname === 'localhost' ||
// [::1] is the IPv6 localhost address.
window.location.hostname === '[::1]' ||
// 127.0.0.1/8 is considered localhost for IPv4.
window.location.hostname.match(
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
)
);
export function register(config) {
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
// The URL constructor is available in all browsers that support SW.
const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
if (publicUrl.origin !== window.location.origin) {
// Our service worker won't work if PUBLIC_URL is on a different origin
// from what our page is served on. This might happen if a CDN is used to
// serve assets; see https://github.com/facebook/create-react-app/issues/2374
return;
}
window.addEventListener('load', () => {
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
if (isLocalhost) {
// This is running on localhost. Let's check if a service worker still exists or not.
checkValidServiceWorker(swUrl, config);
// Add some additional logging to localhost, pointing developers to the
// service worker/PWA documentation.
navigator.serviceWorker.ready.then(() => {
console.log(
'This web app is being served cache-first by a service ' +
'worker. To learn more, visit https://bit.ly/CRA-PWA'
);
});
} else {
// Is not localhost. Just register service worker
registerValidSW(swUrl, config);
}
});
}
}
function registerValidSW(swUrl, config) {
navigator.serviceWorker
.register(swUrl)
.then(registration => {
registration.onupdatefound = () => {
const installingWorker = registration.installing;
if (installingWorker == null) {
return;
}
installingWorker.onstatechange = () => {
if (installingWorker.state === 'installed') {
if (navigator.serviceWorker.controller) {
// At this point, the updated precached content has been fetched,
// but the previous service worker will still serve the older
// content until all client tabs are closed.
console.log(
'New content is available and will be used when all ' +
'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
);
// Execute callback
if (config && config.onUpdate) {
config.onUpdate(registration);
}
} else {
// At this point, everything has been precached.
// It's the perfect time to display a
// "Content is cached for offline use." message.
console.log('Content is cached for offline use.');
// Execute callback
if (config && config.onSuccess) {
config.onSuccess(registration);
}
}
}
};
};
})
.catch(error => {
console.error('Error during service worker registration:', error);
});
}
function checkValidServiceWorker(swUrl, config) {
// Check if the service worker can be found. If it can't reload the page.
fetch(swUrl)
.then(response => {
// Ensure service worker exists, and that we really are getting a JS file.
const contentType = response.headers.get('content-type');
if (
response.status === 404 ||
(contentType != null && contentType.indexOf('javascript') === -1)
) {
// No service worker found. Probably a different app. Reload the page.
navigator.serviceWorker.ready.then(registration => {
registration.unregister().then(() => {
window.location.reload();
});
});
} else {
// Service worker found. Proceed as normal.
registerValidSW(swUrl, config);
}
})
.catch(() => {
console.log(
'No internet connection found. App is running in offline mode.'
);
});
}
export function unregister() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.ready.then(registration => {
registration.unregister();
});
}
}

View File

@ -0,0 +1,3 @@
export const errorMessage = (e) => {
return `Error: ${e.message} ${e.data?.message ? '- Details: ' + e.data.message : ''}`
}

View File

@ -0,0 +1 @@
File marking the root of Wasp project.

View File

@ -0,0 +1,3 @@
# Ignore editor tmp files
**/*~
**/#*#

View File

@ -0,0 +1,81 @@
* {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
box-sizing: border-box;
}
.container {
min-height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
main {
padding: 5rem 0;
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
main p {
font-size: 1.2rem;
}
.logo {
margin-bottom: 2rem;
}
.logo img {
max-height: 200px;
}
.welcome-title {
font-weight: 500;
}
.welcome-subtitle {
font-weight: 400;
margin-bottom: 3rem;
}
.buttons {
display: flex;
flex-direction: row;
}
.buttons .button:not(:last-child) {
margin-right: 0.5rem;
}
.button {
border-radius: 3px;
font-size: 1.2rem;
padding: 1rem 2rem;
text-align: center;
font-weight: 700;
text-decoration: none;
}
.button-filled {
border: 2px solid #bf9900;
background-color: #bf9900;
color: #f4f4f4;
}
.button-outline {
border: 2px solid #8a9cff;
color: #8a9cff;
background-color: none;
}
code {
border-radius: 5px;
padding: 0.2rem;
background: #efefef;
font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono,
Bitstream Vera Sans Mono, Courier New, monospace;
}

View File

@ -0,0 +1,41 @@
import React from 'react'
import waspLogo from './waspLogo.png'
import './Main.css'
const MainPage = () => {
return (
<div className="container">
<main>
<div className="logo">
<img src={waspLogo} alt="wasp" />
</div>
<h2 className="welcome-title"> Welcome to Wasp - you just started a new app! </h2>
<h3 className="welcome-subtitle">
This is page <code>MainPage</code> located at route <code>/</code>.
Open <code>ext/MainPage.js</code> to edit it.
</h3>
<div className="buttons">
<a
className="button button-filled"
href="https://wasp-lang.dev/docs/tutorials/todo-app"
target="_blank"
rel="noreferrer noopener"
>
Take the Tutorial
</a>
<a
className="button button-outline"
href="https://discord.com/invite/rzdnErX"
target="_blank"
rel="noreferrer noopener"
>
Chat on Discord
</a>
</div>
</main>
</div>
)
}
export default MainPage

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View File

@ -0,0 +1,8 @@
app waspCompile {
title: "waspCompile"
}
route RootRoute { path: "/", to: MainPage }
page MainPage {
component: import Main from "@ext/MainPage.js"
}

View File

@ -0,0 +1,54 @@
waspMigrate/.wasp/out/.dockerignore
waspMigrate/.wasp/out/Dockerfile
waspMigrate/.wasp/out/db/migrations/migration_lock.toml
waspMigrate/.wasp/out/db/migrations/no-date-foo/migration.sql
waspMigrate/.wasp/out/db/package.json
waspMigrate/.wasp/out/db/schema.prisma
waspMigrate/.wasp/out/db/schema.prisma.wasp-generate-checksum
waspMigrate/.wasp/out/db/schema.prisma.wasp-migrate-checksum
waspMigrate/.wasp/out/installedFullStackNpmDependencies.json
waspMigrate/.wasp/out/server/.npmrc
waspMigrate/.wasp/out/server/.nvmrc
waspMigrate/.wasp/out/server/README.md
waspMigrate/.wasp/out/server/package.json
waspMigrate/.wasp/out/server/src/app.js
waspMigrate/.wasp/out/server/src/config.js
waspMigrate/.wasp/out/server/src/core/AuthError.js
waspMigrate/.wasp/out/server/src/core/HttpError.js
waspMigrate/.wasp/out/server/src/dbClient.js
waspMigrate/.wasp/out/server/src/ext-src/Main.css
waspMigrate/.wasp/out/server/src/ext-src/MainPage.js
waspMigrate/.wasp/out/server/src/ext-src/waspLogo.png
waspMigrate/.wasp/out/server/src/routes/index.js
waspMigrate/.wasp/out/server/src/routes/operations/index.js
waspMigrate/.wasp/out/server/src/server.js
waspMigrate/.wasp/out/server/src/utils.js
waspMigrate/.wasp/out/web-app/README.md
waspMigrate/.wasp/out/web-app/netlify.toml
waspMigrate/.wasp/out/web-app/package.json
waspMigrate/.wasp/out/web-app/public/favicon.ico
waspMigrate/.wasp/out/web-app/public/index.html
waspMigrate/.wasp/out/web-app/public/manifest.json
waspMigrate/.wasp/out/web-app/src/api.js
waspMigrate/.wasp/out/web-app/src/config.js
waspMigrate/.wasp/out/web-app/src/ext-src/Main.css
waspMigrate/.wasp/out/web-app/src/ext-src/MainPage.js
waspMigrate/.wasp/out/web-app/src/ext-src/waspLogo.png
waspMigrate/.wasp/out/web-app/src/index.css
waspMigrate/.wasp/out/web-app/src/index.js
waspMigrate/.wasp/out/web-app/src/logo.png
waspMigrate/.wasp/out/web-app/src/operations/index.js
waspMigrate/.wasp/out/web-app/src/operations/resources.js
waspMigrate/.wasp/out/web-app/src/queries/index.js
waspMigrate/.wasp/out/web-app/src/queryCache.js
waspMigrate/.wasp/out/web-app/src/router.js
waspMigrate/.wasp/out/web-app/src/serviceWorker.js
waspMigrate/.wasp/out/web-app/src/utils.js
waspMigrate/.wasproot
waspMigrate/ext/.waspignore
waspMigrate/ext/Main.css
waspMigrate/ext/MainPage.js
waspMigrate/ext/waspLogo.png
waspMigrate/main.wasp
waspMigrate/migrations/migration_lock.toml
waspMigrate/migrations/no-date-foo/migration.sql

View File

@ -0,0 +1,2 @@
**/node_modules/
**/.git

View File

@ -0,0 +1,29 @@
FROM node:14-alpine AS node
FROM node AS base
RUN apk --no-cache -U upgrade # To ensure any potential security patches are applied.
FROM base AS server-builder
# Install packages needed to build native npm packages.
RUN apk add --no-cache build-base libtool autoconf automake python
WORKDIR /app
# Install npm packages, resulting in node_modules/.
COPY server/package*.json ./server/
RUN cd server && npm install
COPY db/schema.prisma ./db/
RUN cd server && npx prisma generate --schema=../db/schema.prisma
# TODO: Use pm2?
# TODO: Use non-root user (node).
FROM base AS server-production
ENV NODE_ENV production
WORKDIR /app
COPY --from=server-builder /app/server/node_modules ./server/node_modules
COPY server/ ./server/
COPY db/ ./db/
EXPOSE ${PORT}
WORKDIR /app/server
ENTRYPOINT ["npm", "run", "start-production"]

Some files were not shown because too many files have changed in this diff Show More