Adds golden output dirs for E2E testing (Part 2) (#475)
2
waspc/e2e-test/test-outputs/.gitignore
vendored
@ -1,3 +1 @@
|
||||
*-current
|
||||
# TODO: undo below once PR clean and generate golden dirs for a second PR
|
||||
*-golden
|
||||
|
47
waspc/e2e-test/test-outputs/waspBuild-golden/files.manifest
Normal 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
|
@ -0,0 +1,2 @@
|
||||
**/node_modules/
|
||||
**/.git
|
@ -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"]
|
@ -0,0 +1,11 @@
|
||||
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
output = "../server/node_modules/.prisma/client"
|
||||
}
|
||||
|
@ -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":[]}}
|
@ -0,0 +1 @@
|
||||
engine-strict=true
|
@ -0,0 +1 @@
|
||||
v12.18.0
|
@ -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.
|
@ -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"
|
||||
}
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -0,0 +1,13 @@
|
||||
import Prisma from '@prisma/client'
|
||||
|
||||
|
||||
const createDbClient = () => {
|
||||
const prismaClient = new Prisma.PrismaClient()
|
||||
|
||||
|
||||
return prismaClient
|
||||
}
|
||||
|
||||
const dbClient = createDbClient()
|
||||
|
||||
export default dbClient
|
@ -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;
|
||||
}
|
@ -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
|
After Width: | Height: | Size: 24 KiB |
@ -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
|
@ -0,0 +1,8 @@
|
||||
import express from 'express'
|
||||
|
||||
|
||||
|
||||
const router = express.Router()
|
||||
|
||||
|
||||
export default router
|
@ -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
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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 can’t go back!**
|
||||
|
||||
If you aren’t 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 you’re on your own.
|
||||
|
||||
You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t 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
|
@ -0,0 +1,8 @@
|
||||
[build]
|
||||
publish = "build/"
|
||||
|
||||
[[redirects]]
|
||||
from = "/*"
|
||||
to = "/index.html"
|
||||
status = 200
|
||||
force = false
|
@ -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"
|
||||
}
|
After Width: | Height: | Size: 15 KiB |
@ -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>
|
@ -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"
|
||||
}
|
@ -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
|
@ -0,0 +1,6 @@
|
||||
|
||||
const config = {
|
||||
apiUrl: process.env.REACT_APP_API_URL || 'http://localhost:3001'
|
||||
}
|
||||
|
||||
export default config
|
@ -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;
|
||||
}
|
@ -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
|
After Width: | Height: | Size: 24 KiB |
@ -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;
|
||||
}
|
@ -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();
|
After Width: | Height: | Size: 818 B |
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
import { QueryCache } from 'react-query'
|
||||
|
||||
export default new QueryCache()
|
@ -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
|
@ -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();
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
export const errorMessage = (e) => {
|
||||
return `Error: ${e.message} ${e.data?.message ? '- Details: ' + e.data.message : ''}`
|
||||
}
|
@ -0,0 +1 @@
|
||||
File marking the root of Wasp project.
|
@ -0,0 +1,3 @@
|
||||
# Ignore editor tmp files
|
||||
**/*~
|
||||
**/#*#
|
@ -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;
|
||||
}
|
@ -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
|
After Width: | Height: | Size: 24 KiB |
@ -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"
|
||||
}
|
@ -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
|
@ -0,0 +1,2 @@
|
||||
**/node_modules/
|
||||
**/.git
|
@ -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"]
|
@ -0,0 +1,11 @@
|
||||
|
||||
datasource db {
|
||||
provider = "sqlite"
|
||||
url = "file:./dev.db"
|
||||
}
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
output = "../server/node_modules/.prisma/client"
|
||||
}
|
||||
|
@ -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":[]}}
|
@ -0,0 +1 @@
|
||||
engine-strict=true
|
@ -0,0 +1 @@
|
||||
v12.18.0
|
@ -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.
|
@ -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"
|
||||
}
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -0,0 +1,13 @@
|
||||
import Prisma from '@prisma/client'
|
||||
|
||||
|
||||
const createDbClient = () => {
|
||||
const prismaClient = new Prisma.PrismaClient()
|
||||
|
||||
|
||||
return prismaClient
|
||||
}
|
||||
|
||||
const dbClient = createDbClient()
|
||||
|
||||
export default dbClient
|
@ -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;
|
||||
}
|
@ -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
|
After Width: | Height: | Size: 24 KiB |
@ -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
|
@ -0,0 +1,8 @@
|
||||
import express from 'express'
|
||||
|
||||
|
||||
|
||||
const router = express.Router()
|
||||
|
||||
|
||||
export default router
|
@ -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
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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 can’t go back!**
|
||||
|
||||
If you aren’t 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 you’re on your own.
|
||||
|
||||
You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t 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
|
@ -0,0 +1,8 @@
|
||||
[build]
|
||||
publish = "build/"
|
||||
|
||||
[[redirects]]
|
||||
from = "/*"
|
||||
to = "/index.html"
|
||||
status = 200
|
||||
force = false
|
@ -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"
|
||||
}
|
After Width: | Height: | Size: 15 KiB |
@ -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>
|
@ -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"
|
||||
}
|
@ -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
|
@ -0,0 +1,6 @@
|
||||
|
||||
const config = {
|
||||
apiUrl: process.env.REACT_APP_API_URL || 'http://localhost:3001'
|
||||
}
|
||||
|
||||
export default config
|
@ -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;
|
||||
}
|
@ -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
|
After Width: | Height: | Size: 24 KiB |
@ -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;
|
||||
}
|
@ -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();
|
After Width: | Height: | Size: 818 B |
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
import { QueryCache } from 'react-query'
|
||||
|
||||
export default new QueryCache()
|
@ -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
|
@ -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();
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
export const errorMessage = (e) => {
|
||||
return `Error: ${e.message} ${e.data?.message ? '- Details: ' + e.data.message : ''}`
|
||||
}
|
@ -0,0 +1 @@
|
||||
File marking the root of Wasp project.
|
@ -0,0 +1,3 @@
|
||||
# Ignore editor tmp files
|
||||
**/*~
|
||||
**/#*#
|
@ -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;
|
||||
}
|
@ -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
|
After Width: | Height: | Size: 24 KiB |
@ -0,0 +1,8 @@
|
||||
app waspCompile {
|
||||
title: "waspCompile"
|
||||
}
|
||||
|
||||
route RootRoute { path: "/", to: MainPage }
|
||||
page MainPage {
|
||||
component: import Main from "@ext/MainPage.js"
|
||||
}
|
@ -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
|
@ -0,0 +1,2 @@
|
||||
**/node_modules/
|
||||
**/.git
|
@ -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"]
|