mirror of
https://github.com/wasp-lang/wasp.git
synced 2024-12-24 01:22:24 +03:00
Merge remote-tracking branch 'waspc-private/in-dir'
This commit is contained in:
commit
2ba0e89f05
6
waspc/.gitignore
vendored
Normal file
6
waspc/.gitignore
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
.stack-work/
|
||||
waspc.cabal
|
||||
*~
|
||||
/.dir-locals.el
|
||||
stack*.yaml.lock
|
||||
/out
|
3
waspc/ChangeLog.md
Normal file
3
waspc/ChangeLog.md
Normal file
@ -0,0 +1,3 @@
|
||||
# Changelog
|
||||
|
||||
## Unreleased changes
|
30
waspc/LICENSE
Normal file
30
waspc/LICENSE
Normal file
@ -0,0 +1,30 @@
|
||||
Copyright Author name here (c) 2019
|
||||
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following
|
||||
disclaimer in the documentation and/or other materials provided
|
||||
with the distribution.
|
||||
|
||||
* Neither the name of Author name here nor the names of other
|
||||
contributors may be used to endorse or promote products derived
|
||||
from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
110
waspc/README.md
Normal file
110
waspc/README.md
Normal file
@ -0,0 +1,110 @@
|
||||
```
|
||||
_ _
|
||||
__ ____ _ ___ _ __ ___ | )/ )
|
||||
\ \ /\ / / _` / __| '_ \ / __| \\ |//,' __
|
||||
\ V V / (_| \__ \ |_) | (__ (")(_)-"()))=-
|
||||
\_/\_/ \__,_|___/ .__/ \___| (\\
|
||||
|_|
|
||||
```
|
||||
|
||||
## Setup
|
||||
Install `stack`.
|
||||
|
||||
Run `stack setup` in the project root to do initial setup.
|
||||
|
||||
## Project configuration overview
|
||||
This is a [Stack](https://docs.haskellstack.org/en/stable/README/) project.
|
||||
|
||||
Stack installs GHC and packages automatically, in reproducible manner, in an isolated environment just for your Haskell project.
|
||||
|
||||
This project was created from `new-template` stack template and then modified inspired by `haskeleton` stack template.
|
||||
|
||||
Most important configuration file is `package.yaml`. Here we define what is what in our project, how is it built. Stack generates `waspc.cabal` from this document.
|
||||
|
||||
Also important is `stack.yaml` where we define Stack configuration for this project.
|
||||
|
||||
We provided some pieces of Stack documentation in this README to make it easier to start with the project, but for any more detailed and guaranteed up-to-date docs check Stack docs at https://docs.haskellstack.org/en/stable/README/.
|
||||
|
||||
### Adding a package as a dependency
|
||||
This is just so you don't have to search Stack docs.
|
||||
|
||||
Just put it in package.yaml like this:
|
||||
```
|
||||
dependencies:
|
||||
...
|
||||
- <package_name>
|
||||
...
|
||||
```
|
||||
|
||||
If package you need is not in the Stack snapshot defined by `resolver`, add it to `extra-deps` instead of `dependencies`.
|
||||
|
||||
|
||||
## Building / development
|
||||
You build the project with `stack build`. It uses `package.yaml`, `waspc.yaml` (and possibly some other files in the future) and source files to generate files and build the project.
|
||||
|
||||
It is recommended using `stack build --pedantic` to turn on pedantic code checking (-Wall, -Werror).
|
||||
|
||||
`stack exec <my-executable>` will run executable in the context of stack project.
|
||||
In our case, `stack exec waspc-cli` will run waspc (build it first with `stack build`!).
|
||||
|
||||
Some useful command options when building:
|
||||
- `stack build --test` -> same as `stack test`.
|
||||
- `stack build --file-watch` -> live watch, reruns every time a file changes.
|
||||
- `stack build --pedantic` -> sets -Wall and -Werror ghc options.
|
||||
- `stack build --ghc-options="-Wall -Werror"` -> sets ghc options.
|
||||
- `stack build --bench`
|
||||
- `stack build --profile`
|
||||
- `stack build --trace`
|
||||
|
||||
There is also `stack install` which builds the project and then copies it to the local bin path.
|
||||
|
||||
Running `stack ghci` will open ghci in the context of the project, allowing you to load and run local modules, which can be useful for development.
|
||||
|
||||
You can use `stack clear` to clear all the generated files/artifacts from the project.
|
||||
|
||||
When developing, if you don't have full support for Haskell in your editor, consider using `stack build --file-watch` for live error checking, or `ghcid` as a faster alternative (install it globally with `stack install ghcid` and then just type `ghcid` when in the project.
|
||||
|
||||
### Run script
|
||||
For more convenient running of common build/dev commands, we created `run` script.
|
||||
It mostly runs stack commands described above.
|
||||
|
||||
The idea is that you normally use this for development, and you use `stack` directly when you need more control.
|
||||
|
||||
You can run `./run help` to learn how to use it.
|
||||
|
||||
Examples:
|
||||
- `./run exec examples/todoMVC.wasp out/todoMVC` will run waspc on todoMVC example.
|
||||
|
||||
|
||||
## Tests
|
||||
For tests we are using [**Tasty**](https://documentup.com/feuerbach/tasty) testing framework. Tasty let's us combine different types of tests into a single test suite.
|
||||
|
||||
In Tasty, there is a main test file that is run when test suite is run. In that file we need to manually compose test tree out of tests that we wrote. We organize tests in test groups, which are then recursively grouped resulting in a test tree.
|
||||
Cool thing is that we can organize tests this way however we want and also mix different type of tests (hspec, quickcheck, and whatever else want really).
|
||||
|
||||
Tests are normally split in files of course, so we need to import those all the way up to the main test file, however we organize our test groups/trees.
|
||||
|
||||
In order to avoid need for manual organization and importing of test files described above, we are using [tasty-discover](https://hackage.haskell.org/package/tasty-discover) which does this for us. It automatically detects files containing tests and then organizes them for us into test tree (and also takes care of importing). This means we only need to create a file, write tests in it and that is it.
|
||||
Test functions however do need to be prefixed with special prefix to indicate which type of test are they: spec_ for Hspec, prop_ for QuickCheck and similar. Check docs for more details.
|
||||
We can however still organize tests manually if we want in Tasty test trees, and then we just prefix them with test_ and tasty-discover will pick them up from there.
|
||||
|
||||
Additionally, currently we limited tasty-discover to auto-detect only files ending with Test.hs (*Test.hs glob). We might remove that requirement in the future if it proves to have no benefit.
|
||||
|
||||
For unit testing, we use **Hspec**.
|
||||
|
||||
For property testing, we use **Quickcheck**.
|
||||
|
||||
We additionally use **doctest** for testing code examples in documentation.
|
||||
|
||||
All tests go into `test/` directory. This is convention for Haskell, opposite to mixing them with source code as in Javascript for example. Not only that, but Haskell build tools don't have a good support for mixing them with source files, so even if we wanted to do that it is just not worth the hassle.
|
||||
|
||||
Test are run with `stack test`. You can do `stack test --coverage` to see the coverage.
|
||||
|
||||
## Benchmarking
|
||||
For benchmarking we are using [**Criterion**](http://www.serpentine.com/criterion/).
|
||||
|
||||
You can run benchmark with `stack bench`.
|
||||
|
||||
## Other
|
||||
|
||||
Wasp ascii art used in the title is from https://www.asciiart.eu/animals/insects/bees, author: Stef00.
|
2
waspc/Setup.hs
Normal file
2
waspc/Setup.hs
Normal file
@ -0,0 +1,2 @@
|
||||
import Distribution.Simple
|
||||
main = defaultMain
|
34
waspc/app/Main.hs
Normal file
34
waspc/app/Main.hs
Normal file
@ -0,0 +1,34 @@
|
||||
module Main where
|
||||
|
||||
import System.Environment
|
||||
import System.Directory
|
||||
import qualified System.FilePath as FilePath
|
||||
import Path ((</>), reldir)
|
||||
import qualified Path
|
||||
import qualified Path.Aliases as Path
|
||||
|
||||
import CompileOptions (CompileOptions (..))
|
||||
import Lib (compile)
|
||||
|
||||
|
||||
main :: IO ()
|
||||
main = do
|
||||
absCwdPath <- getCurrentDirectory >>= Path.parseAbsDir
|
||||
args <- getArgs
|
||||
case args of
|
||||
[waspFilePath, outDirPath] -> do
|
||||
absWaspFilePath <- Path.parseAbsFile (ensurePathIsAbs absCwdPath waspFilePath)
|
||||
absOutDirPath <- Path.parseAbsDir (ensurePathIsAbs absCwdPath outDirPath)
|
||||
-- TODO(martin): Take compile options as arguments to the command, right now I hardcoded the value.
|
||||
let options = CompileOptions
|
||||
{ externalCodeDirPath = (Path.parent absWaspFilePath) </> [reldir|ext|]
|
||||
}
|
||||
result <- compile absWaspFilePath absOutDirPath options
|
||||
either putStrLn (\_ -> print ("Success!" :: String)) result
|
||||
_ -> print ("Usage: ./waspc <wasp_file_path> <out_dir>" :: String)
|
||||
where
|
||||
-- | If path is not absolute, it is prefixed with given absolute directory.
|
||||
ensurePathIsAbs :: Path.AbsDir -> FilePath -> FilePath
|
||||
ensurePathIsAbs absDirPath path = if FilePath.isAbsolute path
|
||||
then path
|
||||
else (Path.toFilePath absDirPath) FilePath.</> path
|
6
waspc/benchmark/Main.hs
Normal file
6
waspc/benchmark/Main.hs
Normal file
@ -0,0 +1,6 @@
|
||||
-- You can benchmark your code quickly and effectively with Criterion. See its
|
||||
-- website for help: <http://www.serpentine.com/criterion/>.
|
||||
import Criterion.Main
|
||||
|
||||
main :: IO ()
|
||||
main = defaultMain [bench "const" (whnf const ())]
|
69
waspc/data/Generator/templates/react-app/README.md
Normal file
69
waspc/data/Generator/templates/react-app/README.md
Normal file
@ -0,0 +1,69 @@
|
||||
{{={{> <}}=}}
|
||||
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
|
24
waspc/data/Generator/templates/react-app/gitignore
Normal file
24
waspc/data/Generator/templates/react-app/gitignore
Normal file
@ -0,0 +1,24 @@
|
||||
{{={{> <}}=}}
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
39
waspc/data/Generator/templates/react-app/package.json
Normal file
39
waspc/data/Generator/templates/react-app/package.json
Normal file
@ -0,0 +1,39 @@
|
||||
{{={= =}=}}
|
||||
{
|
||||
"name": "{= app.name =}",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@material-ui/core": "^4.9.1",
|
||||
"@reduxjs/toolkit": "^1.2.3",
|
||||
"lodash": "^4.17.15",
|
||||
"react": "^16.12.0",
|
||||
"react-dom": "^16.12.0",
|
||||
"react-redux": "^7.1.3",
|
||||
"react-router-dom": "^5.1.2",
|
||||
"react-scripts": "3.3.1",
|
||||
"redux": "^4.0.5",
|
||||
"uuid": "^3.4.0"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": "react-app"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
}
|
||||
}
|
BIN
waspc/data/Generator/templates/react-app/public/favicon.ico
Normal file
BIN
waspc/data/Generator/templates/react-app/public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
53
waspc/data/Generator/templates/react-app/public/index.html
Normal file
53
waspc/data/Generator/templates/react-app/public/index.html
Normal file
@ -0,0 +1,53 @@
|
||||
{{={{> <}}=}}
|
||||
<!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" />
|
||||
|
||||
<!--
|
||||
This is the font that Material-UI requires.
|
||||
-->
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500&display=swap" />
|
||||
|
||||
<!--
|
||||
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>{{> app.title <}}</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,15 @@
|
||||
{{={{> <}}=}}
|
||||
{
|
||||
"name": "{{> app.name <}}",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
}
|
46
waspc/data/Generator/templates/react-app/src/_Page.js
vendored
Normal file
46
waspc/data/Generator/templates/react-app/src/_Page.js
vendored
Normal file
@ -0,0 +1,46 @@
|
||||
{{={= =}=}}
|
||||
import React, { Component } from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
|
||||
{=# jsImports =}
|
||||
import {= what =} from "{= from =}"
|
||||
{=/ jsImports =}
|
||||
|
||||
{=# entities =}
|
||||
import * as {= entityLowerName =}State from '{= entityStatePath =}'
|
||||
import * as {= entityLowerName =}Actions from '{= entityActionsPath =}'
|
||||
import {= entity.name =} from '{= entityClassPath =}'
|
||||
{=# entityCreateForms =}
|
||||
import {= entityForm.name =} from '{= path =}'
|
||||
{=/ entityCreateForms =}
|
||||
{=# entityLists =}
|
||||
import {= entityList.name =} from '{= path =}'
|
||||
{=/ entityLists =}
|
||||
{=/ entities =}
|
||||
|
||||
{=# pageStylePath =}
|
||||
import '{= pageStylePath =}'
|
||||
{=/ pageStylePath =}
|
||||
|
||||
|
||||
export class {= page.name =} extends Component {
|
||||
// TODO: Add propTypes.
|
||||
|
||||
render() {
|
||||
return (
|
||||
{=& page.content =}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(state => ({
|
||||
{=# entities =}
|
||||
{= entityLowerName =}List: {= entityLowerName =}State.selectors.all(state)
|
||||
{=/ entities =}
|
||||
}), {
|
||||
{=# entities =}
|
||||
add{= entityUpperName =}: {= entityLowerName =}Actions.add,
|
||||
update{= entityUpperName =}: {= entityLowerName =}Actions.update,
|
||||
remove{= entityUpperName =}: {= entityLowerName =}Actions.remove
|
||||
{=/ entities =}
|
||||
})({= page.name =})
|
29
waspc/data/Generator/templates/react-app/src/entities/_entity/_Entity.js
vendored
Normal file
29
waspc/data/Generator/templates/react-app/src/entities/_entity/_Entity.js
vendored
Normal file
@ -0,0 +1,29 @@
|
||||
{{={= =}=}}
|
||||
import uuidv4 from 'uuid/v4'
|
||||
|
||||
export default class {= entity.name =} {
|
||||
_data = {}
|
||||
|
||||
constructor (data = {}) {
|
||||
this._data = {
|
||||
id: data.id || uuidv4(),
|
||||
{=# entity.fields =}
|
||||
{= name =}: data.{= name =},
|
||||
{=/ entity.fields =}
|
||||
}
|
||||
}
|
||||
|
||||
get id () {
|
||||
return this._data.id
|
||||
}
|
||||
|
||||
{=# entity.fields =}
|
||||
get {= name =} () {
|
||||
return this._data.{= name =}
|
||||
}
|
||||
{=/ entity.fields =}
|
||||
|
||||
toData () {
|
||||
return this._data
|
||||
}
|
||||
}
|
4
waspc/data/Generator/templates/react-app/src/entities/_entity/actionTypes.js
vendored
Normal file
4
waspc/data/Generator/templates/react-app/src/entities/_entity/actionTypes.js
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
{{={= =}=}}
|
||||
export const ADD = 'entities/{= entityLowerName =}/ADD'
|
||||
export const UPDATE = 'entities/{= entityLowerName =}/UPDATE'
|
||||
export const REMOVE = 'entities/{= entityLowerName =}/REMOVE'
|
29
waspc/data/Generator/templates/react-app/src/entities/_entity/actions.js
vendored
Normal file
29
waspc/data/Generator/templates/react-app/src/entities/_entity/actions.js
vendored
Normal file
@ -0,0 +1,29 @@
|
||||
{{={= =}=}}
|
||||
import * as types from './actionTypes'
|
||||
|
||||
|
||||
/**
|
||||
* @param {{= entity.name =}} {= entityLowerName =}
|
||||
*/
|
||||
export const add = ({= entityLowerName =}) => ({
|
||||
type: types.ADD,
|
||||
data: {= entityLowerName =}.toData()
|
||||
})
|
||||
|
||||
/**
|
||||
* @param {String} id
|
||||
* @param {Object} data - Partial data that will be merged with existing {= entityLowerName =}.
|
||||
*/
|
||||
export const update = (id, data) => ({
|
||||
type: types.UPDATE,
|
||||
id,
|
||||
data
|
||||
})
|
||||
|
||||
/**
|
||||
* @param {String} id
|
||||
*/
|
||||
export const remove = (id) => ({
|
||||
type: types.REMOVE,
|
||||
id
|
||||
})
|
122
waspc/data/Generator/templates/react-app/src/entities/_entity/components/CreateForm.js
vendored
Normal file
122
waspc/data/Generator/templates/react-app/src/entities/_entity/components/CreateForm.js
vendored
Normal file
@ -0,0 +1,122 @@
|
||||
{{={= =}=}}
|
||||
import _ from 'lodash'
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import FormControlLabel from '@material-ui/core/FormControlLabel'
|
||||
import Switch from '@material-ui/core/Switch'
|
||||
import Button from '@material-ui/core/Button'
|
||||
import TextField from '@material-ui/core/TextField'
|
||||
|
||||
import {= entityClassName =} from '../{= entityClassName =}'
|
||||
|
||||
|
||||
export default class {= formName =} extends React.Component {
|
||||
static propTypes = {
|
||||
onCreate: PropTypes.func,
|
||||
submitButtonLabel: PropTypes.string
|
||||
}
|
||||
|
||||
state = {
|
||||
fields: {
|
||||
{=# formFields =}
|
||||
{=# boolean =}
|
||||
{= name =}: {= defaultValue =},
|
||||
{=/ boolean =}
|
||||
{=# string =}
|
||||
{= name =}: '{= defaultValue =}',
|
||||
{=/ string =}
|
||||
{=/ formFields =}
|
||||
}
|
||||
}
|
||||
|
||||
setField = (name, valueOrFn) => {
|
||||
this.setState(prevState => ({
|
||||
fields: {
|
||||
...prevState.fields,
|
||||
[name]: _.isFunction(valueOrFn) ? valueOrFn(prevState) : valueOrFn
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
resetAllFields = () => {
|
||||
{=# formFields =}
|
||||
{=# boolean =}
|
||||
this.setField('{= name =}', {= defaultValue =})
|
||||
{=/ boolean =}
|
||||
{=# string =}
|
||||
this.setField('{= name =}', '{= defaultValue =}')
|
||||
{=/ string =}
|
||||
{=/ formFields =}
|
||||
}
|
||||
|
||||
toggleField = (name) => {
|
||||
this.setField(name, prevState => !prevState.fields[name])
|
||||
}
|
||||
|
||||
getField = (name) => {
|
||||
return this.state.fields[name]
|
||||
}
|
||||
|
||||
handleSubmit = () => {
|
||||
this.props.onCreate(new {= entityClassName =}(this.state.fields))
|
||||
this.resetAllFields()
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className={this.props.className}>
|
||||
<form noValidate onSubmit={this.handleSubmit} action="javascript:void(0);">
|
||||
{=# formFields =}
|
||||
{=# boolean =}
|
||||
{=# show =}
|
||||
<div>
|
||||
<FormControlLabel
|
||||
{=# label =}
|
||||
label="{= label =}"
|
||||
{=/ label =}
|
||||
control={
|
||||
<Switch
|
||||
checked={this.getField('{= name =}')}
|
||||
onChange={() => this.toggleField('{= name =}')}
|
||||
value="{= name =}"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
{=/ show =}
|
||||
{=/ boolean =}
|
||||
{=# string =}
|
||||
{=# show =}
|
||||
<div>
|
||||
<TextField
|
||||
{=# label =}
|
||||
label="{= label =}"
|
||||
{=/ label =}
|
||||
{=# placeholder =}
|
||||
placeholder="{= placeholder =}"
|
||||
{=/ placeholder =}
|
||||
value={this.getField('{= name =}')}
|
||||
onChange={event => this.setField('{= name =}', event.target.value)}
|
||||
margin="normal"
|
||||
fullWidth
|
||||
InputLabelProps={{
|
||||
shrink: true
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{=/ show =}
|
||||
{=/ string =}
|
||||
{=/ formFields =}
|
||||
{=# showSubmitButton =}
|
||||
<div>
|
||||
<Button type="submit" variant="contained" color="primary">
|
||||
{this.props.submitButtonLabel || 'Submit'}
|
||||
</Button>
|
||||
</div>
|
||||
{=/ showSubmitButton =}
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
130
waspc/data/Generator/templates/react-app/src/entities/_entity/components/List.js
vendored
Normal file
130
waspc/data/Generator/templates/react-app/src/entities/_entity/components/List.js
vendored
Normal file
@ -0,0 +1,130 @@
|
||||
{{={= =}=}}
|
||||
import _ from 'lodash'
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { connect } from 'react-redux'
|
||||
|
||||
import Paper from '@material-ui/core/Paper'
|
||||
import Table from '@material-ui/core/Table'
|
||||
import TableBody from '@material-ui/core/TableBody'
|
||||
import TableCell from '@material-ui/core/TableCell'
|
||||
import TableHead from '@material-ui/core/TableHead'
|
||||
import TableRow from '@material-ui/core/TableRow'
|
||||
import Checkbox from '@material-ui/core/Checkbox'
|
||||
import TextField from '@material-ui/core/TextField'
|
||||
import ClickAwayListener from '@material-ui/core/ClickAwayListener'
|
||||
|
||||
import * as {= entityLowerName =}State from '../state'
|
||||
import * as {= entityLowerName =}Actions from '../actions'
|
||||
|
||||
import {= entityClassName =} from '../{= entityClassName =}'
|
||||
|
||||
|
||||
export class {= listName =} extends React.Component {
|
||||
static propTypes = {
|
||||
editable: PropTypes.bool,
|
||||
filter: PropTypes.func
|
||||
}
|
||||
|
||||
state = {
|
||||
{= entityBeingEditedStateVar =}: null
|
||||
}
|
||||
|
||||
setAsBeingEdited = {= entityLowerName =} => this.setState({
|
||||
{= entityBeingEditedStateVar =}: {= entityLowerName =}.id
|
||||
})
|
||||
|
||||
isBeingEdited = {= entityLowerName =} =>
|
||||
{= entityLowerName =}.id === this.state.{= entityBeingEditedStateVar =}
|
||||
|
||||
finishEditing = {= entityLowerName =} => {
|
||||
if ({= entityLowerName =}.id === this.state.{= entityBeingEditedStateVar =})
|
||||
this.setState({ {= entityBeingEditedStateVar =}: null })
|
||||
}
|
||||
|
||||
{=! Render "render" functions for each field, if provided =}
|
||||
{=# listFields =}
|
||||
{=# render =}
|
||||
{= renderFnName =} =
|
||||
{=& render =}
|
||||
{=/ render =}
|
||||
{=/ listFields =}
|
||||
|
||||
render() {
|
||||
const {= entityLowerName =}ListToShow = this.props.filter ?
|
||||
{=! TODO(matija): duplication, we could extract entityLowerName_List =}
|
||||
this.props.{= entityLowerName =}List.filter(this.props.filter) :
|
||||
this.props.{= entityLowerName =}List
|
||||
|
||||
return (
|
||||
<div className={this.props.className}>
|
||||
<Paper>
|
||||
<Table>
|
||||
<TableHead{=^ showHeader =} style={{display: 'none'}}{=/ showHeader =}>
|
||||
<TableRow>
|
||||
{=# listFields =}
|
||||
<TableCell width="{= widthAsPercent =}%">{= name =}</TableCell>
|
||||
{=/ listFields =}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
|
||||
<TableBody>
|
||||
{{= entityLowerName =}ListToShow.map(({= entityLowerName =}) => (
|
||||
<TableRow key={{= entityLowerName =}.id}>
|
||||
{=# listFields =}
|
||||
{=# boolean =}
|
||||
<TableCell>
|
||||
<Checkbox
|
||||
checked={{= entityLowerName =}.{= name =}}
|
||||
color="default"
|
||||
inputProps={{
|
||||
'aria-label': 'checkbox'
|
||||
}}
|
||||
disabled={!this.props.editable}
|
||||
onChange={e => this.props.update{= entityName =}(
|
||||
{= entityLowerName =}.id, { '{= name =}': e.target.checked }
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
{=/ boolean =}
|
||||
{=# string =}
|
||||
<ClickAwayListener onClickAway={() => this.finishEditing({= entityLowerName =}) }>
|
||||
<TableCell
|
||||
onDoubleClick={() => this.setAsBeingEdited({= entityLowerName =})}
|
||||
>
|
||||
{this.props.editable && this.isBeingEdited({= entityLowerName =}) ? (
|
||||
<TextField
|
||||
value={{= entityLowerName =}.{= name =}}
|
||||
onChange={e => this.props.update{= entityName =}(
|
||||
{= entityLowerName =}.id, { '{= name =}': e.target.value }
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
{=# render =}
|
||||
this.{= renderFnName =}({= entityLowerName =})
|
||||
{=/ render =}
|
||||
{=^ render =}
|
||||
{= entityLowerName =}.{= name =}
|
||||
{=/ render =}
|
||||
)}
|
||||
</TableCell>
|
||||
</ClickAwayListener>
|
||||
{=/ string =}
|
||||
{=/ listFields =}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Paper>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(state => ({
|
||||
// Selectors
|
||||
{= entityLowerName =}List: {= entityLowerName =}State.selectors.all(state)
|
||||
}), {
|
||||
// Actions
|
||||
update{= entityName =}: {= entityLowerName =}Actions.update
|
||||
})({= listName =})
|
60
waspc/data/Generator/templates/react-app/src/entities/_entity/state.js
vendored
Normal file
60
waspc/data/Generator/templates/react-app/src/entities/_entity/state.js
vendored
Normal file
@ -0,0 +1,60 @@
|
||||
{{={= =}=}}
|
||||
import { createSelector } from 'reselect'
|
||||
|
||||
import {= entity.name =} from './{= entity.name =}'
|
||||
import * as types from './actionTypes'
|
||||
|
||||
|
||||
// We assume that root reducer of the app will put this reducer under
|
||||
// key ROOT_REDUCER_KEY.
|
||||
const ROOT_REDUCER_KEY = 'entities/{= entity.name =}'
|
||||
|
||||
const initialState = {
|
||||
all: []
|
||||
}
|
||||
|
||||
const reducer = (state = initialState, action) => {
|
||||
switch (action.type) {
|
||||
case types.ADD:
|
||||
return {
|
||||
...state,
|
||||
all: [ ...state.all, action.data ]
|
||||
}
|
||||
|
||||
case types.UPDATE:
|
||||
return {
|
||||
...state,
|
||||
all: state.all.map(
|
||||
{= entityLowerName =} =>
|
||||
{= entityLowerName =}.id === action.id
|
||||
? { ...{= entityLowerName =}, ...action.data }
|
||||
: {= entityLowerName =}
|
||||
)
|
||||
}
|
||||
|
||||
case types.REMOVE:
|
||||
return {
|
||||
...state,
|
||||
all: state.all.filter(
|
||||
{= entityLowerName =} => {= entityLowerName =}.id !== action.id
|
||||
)
|
||||
}
|
||||
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
let selectors = {}
|
||||
selectors.root = (state) => state[ROOT_REDUCER_KEY]
|
||||
|
||||
/**
|
||||
* @returns {{= entity.name =}[]}
|
||||
*/
|
||||
selectors.all = createSelector(selectors.root, (state) => {
|
||||
return state.all.map(data => new {= entity.name =}(data))
|
||||
})
|
||||
|
||||
|
||||
export { reducer, initialState, selectors, ROOT_REDUCER_KEY }
|
15
waspc/data/Generator/templates/react-app/src/index.css
Normal file
15
waspc/data/Generator/templates/react-app/src/index.css
Normal file
@ -0,0 +1,15 @@
|
||||
{{={{> <}}=}}
|
||||
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;
|
||||
}
|
26
waspc/data/Generator/templates/react-app/src/index.js
vendored
Normal file
26
waspc/data/Generator/templates/react-app/src/index.js
vendored
Normal file
@ -0,0 +1,26 @@
|
||||
{{={= =}=}}
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import { Provider } from 'react-redux'
|
||||
|
||||
import router from './router'
|
||||
import { configureStore } from './store'
|
||||
import { rootReducer } from './reducers'
|
||||
import * as serviceWorker from './serviceWorker'
|
||||
|
||||
import './index.css'
|
||||
|
||||
|
||||
const store = configureStore(rootReducer)
|
||||
|
||||
ReactDOM.render(
|
||||
<Provider store={store}>
|
||||
{ router }
|
||||
</Provider>,
|
||||
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();
|
BIN
waspc/data/Generator/templates/react-app/src/logo.png
Normal file
BIN
waspc/data/Generator/templates/react-app/src/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 818 B |
23
waspc/data/Generator/templates/react-app/src/reducers.js
vendored
Normal file
23
waspc/data/Generator/templates/react-app/src/reducers.js
vendored
Normal file
@ -0,0 +1,23 @@
|
||||
{{={= =}=}}
|
||||
import { combineReducers } from 'redux'
|
||||
|
||||
{=# entities =}
|
||||
import * as {= entityLowerName =}State from '{= entityStatePath =}'
|
||||
{=/ entities =}
|
||||
|
||||
|
||||
const states = [
|
||||
{=# entities =}
|
||||
{= entityLowerName =}State,
|
||||
{=/ entities =}
|
||||
]
|
||||
|
||||
const keyToReducer = states.reduce((acc, state) => {
|
||||
// We set the reducers here by using their ROOT_REDUCER_KEY, because their
|
||||
// internal state implementations assume so.
|
||||
return { ...acc, [state.ROOT_REDUCER_KEY]: state.reducer }
|
||||
}, {})
|
||||
|
||||
export const rootReducer = combineReducers({
|
||||
...keyToReducer
|
||||
})
|
20
waspc/data/Generator/templates/react-app/src/router.js
vendored
Normal file
20
waspc/data/Generator/templates/react-app/src/router.js
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
{{={= =}=}}
|
||||
import React from 'react'
|
||||
import { Route, BrowserRouter as Router } from 'react-router-dom'
|
||||
|
||||
{=# pages =}
|
||||
import {= name =} from './{= name =}'
|
||||
{=/ pages =}
|
||||
|
||||
|
||||
const router = (
|
||||
<Router>
|
||||
<div>
|
||||
{=# pages =}
|
||||
<Route exact path="{= route =}" component={ {= name =} }/>
|
||||
{=/ pages =}
|
||||
</div>
|
||||
</Router>
|
||||
)
|
||||
|
||||
export default router
|
136
waspc/data/Generator/templates/react-app/src/serviceWorker.js
vendored
Normal file
136
waspc/data/Generator/templates/react-app/src/serviceWorker.js
vendored
Normal file
@ -0,0 +1,136 @@
|
||||
{{={{> <}}=}}
|
||||
// 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();
|
||||
});
|
||||
}
|
||||
}
|
27
waspc/data/Generator/templates/react-app/src/store/index.js
vendored
Normal file
27
waspc/data/Generator/templates/react-app/src/store/index.js
vendored
Normal file
@ -0,0 +1,27 @@
|
||||
{{={= =}=}}
|
||||
import * as RTK from '@reduxjs/toolkit'
|
||||
|
||||
import loggerMiddleware from './middleware/logger'
|
||||
|
||||
/**
|
||||
* @param {Function} reducer - Redux reducer to be used for this store.
|
||||
* @param {Object} [preloadedState] - State that this store will be initialized with.
|
||||
* @returns {Object} Redux store, configured to suit our needs.
|
||||
*/
|
||||
export const configureStore = (reducer, preloadedState) => {
|
||||
const middleware = [
|
||||
...RTK.getDefaultMiddleware(),
|
||||
loggerMiddleware
|
||||
]
|
||||
|
||||
const enhancers = []
|
||||
|
||||
const store = RTK.configureStore({
|
||||
reducer,
|
||||
preloadedState,
|
||||
middleware,
|
||||
enhancers
|
||||
})
|
||||
|
||||
return store
|
||||
}
|
14
waspc/data/Generator/templates/react-app/src/store/middleware/logger.js
vendored
Normal file
14
waspc/data/Generator/templates/react-app/src/store/middleware/logger.js
vendored
Normal file
@ -0,0 +1,14 @@
|
||||
{{={= =}=}}
|
||||
const logger = store => next => action => {
|
||||
console.group(action.type)
|
||||
console.info('dispatching', action)
|
||||
|
||||
let result = next(action)
|
||||
|
||||
console.log('next state', store.getState())
|
||||
console.groupEnd()
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
export default logger
|
57
waspc/examples/todoApp/ext/Main.css
Normal file
57
waspc/examples/todoApp/ext/Main.css
Normal file
@ -0,0 +1,57 @@
|
||||
div {
|
||||
color: green;
|
||||
}
|
||||
|
||||
button.selected {
|
||||
border: 1px solid black;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.todos {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.todos__container {
|
||||
width: 450px;
|
||||
}
|
||||
|
||||
.todos__toggleAndInput {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.todos__toggleButton {
|
||||
height: 40px;
|
||||
margin-bottom: 8px;
|
||||
margin-left: 20px;
|
||||
|
||||
background-color: white;
|
||||
border: none;
|
||||
display: inline-block;
|
||||
font-size: 24px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.todos__newTaskForm {
|
||||
width: 100%;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.todos__footer {
|
||||
width: 100%;
|
||||
margin-top: 10px;
|
||||
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.todos__footer__filters .filter:not(:last-child) {
|
||||
margin-right: 10px;
|
||||
}
|
119
waspc/examples/todoApp/ext/Todo.js
Normal file
119
waspc/examples/todoApp/ext/Todo.js
Normal file
@ -0,0 +1,119 @@
|
||||
// As seen here, we can import npm packages used in code generated by Wasp, which will be clearly defined.
|
||||
// In the future, we will of course also be able to specify additional packages as dependencies.
|
||||
import React from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
|
||||
// As seen here, we can import specific components/code generated by Wasp.
|
||||
// These will have well defined and documented APIs and paths.
|
||||
// Note that Task, NewTaskForm and TaskList are generated based on the declarations
|
||||
// we made in todoApp.wasp file.
|
||||
import Task from '@wasp/entities/task/Task'
|
||||
import NewTaskForm from '@wasp/entities/task/components/NewTaskForm'
|
||||
import TaskList from '@wasp/entities/task/components/TaskList'
|
||||
import * as taskState from '@wasp/entities/task/state.js'
|
||||
import * as taskActions from '@wasp/entities/task/actions.js'
|
||||
|
||||
const TASK_FILTER_TYPES = Object.freeze({
|
||||
ALL: 'all',
|
||||
ACTIVE: 'active',
|
||||
COMPLETED: 'completed'
|
||||
})
|
||||
|
||||
const TASK_FILTERS = Object.freeze({
|
||||
[TASK_FILTER_TYPES.ALL]: null,
|
||||
[TASK_FILTER_TYPES.ACTIVE]: task => !task.isDone,
|
||||
[TASK_FILTER_TYPES.COMPLETED]: task => task.isDone
|
||||
})
|
||||
|
||||
class Todo extends React.Component {
|
||||
// TODO: prop types.
|
||||
|
||||
state = {
|
||||
taskFilterName: TASK_FILTER_TYPES.ALL
|
||||
}
|
||||
|
||||
toggleIsDoneForAllTasks = () => {
|
||||
const areAllDone = this.props.taskList.every(t => t.isDone)
|
||||
this.props.taskList.map(t => this.props.updateTask(t.id, { isDone: !areAllDone }))
|
||||
}
|
||||
|
||||
deleteCompletedTasks = () => {
|
||||
this.props.taskList.map((t) => { if (t.isDone) this.props.removeTask(t.id) })
|
||||
}
|
||||
|
||||
isAnyTaskCompleted = () => this.props.taskList.some(t => t.isDone)
|
||||
|
||||
isThereAnyTask = () => this.props.taskList.length > 0
|
||||
|
||||
TaskFilterButton = ({ filterType, label }) => (
|
||||
<button
|
||||
className={'filter ' + (this.state.taskFilterName === filterType ? 'selected' : '')}
|
||||
onClick={() => this.setState({ taskFilterName: filterType })}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
)
|
||||
|
||||
render = () => {
|
||||
return (
|
||||
<div className="todos">
|
||||
<div className="todos__container">
|
||||
<h1> Todos </h1>
|
||||
|
||||
<div className="todos__toggleAndInput">
|
||||
<button
|
||||
disabled={!this.isThereAnyTask()}
|
||||
className="todos__toggleButton"
|
||||
onClick={this.toggleIsDoneForAllTasks}>
|
||||
✓
|
||||
</button>
|
||||
|
||||
<NewTaskForm
|
||||
className="todos__newTaskForm"
|
||||
onCreate={task => this.props.addTask(task)}
|
||||
submitButtonLabel={'Create new task'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{ this.isThereAnyTask() && (<>
|
||||
<TaskList
|
||||
editable
|
||||
filter={TASK_FILTERS[this.state.taskFilterName]}
|
||||
/>
|
||||
|
||||
<div className="todos__footer">
|
||||
<div className="todos__footer__itemsLeft">
|
||||
{ this.props.taskList.filter(task => !task.isDone).length } items left
|
||||
</div>
|
||||
|
||||
<div className="todos__footer__filters">
|
||||
<this.TaskFilterButton filterType={TASK_FILTER_TYPES.ALL} label="All" />
|
||||
<this.TaskFilterButton filterType={TASK_FILTER_TYPES.ACTIVE} label="Active" />
|
||||
<this.TaskFilterButton filterType={TASK_FILTER_TYPES.COMPLETED} label="Completed" />
|
||||
</div>
|
||||
|
||||
<div className="todos__footer__clearCompleted">
|
||||
<button
|
||||
className={this.isAnyTaskCompleted() ? '' : 'hidden' }
|
||||
onClick={this.deleteCompletedTasks}>
|
||||
Clear completed
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</>)}
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(state => ({
|
||||
taskList: taskState.selectors.all(state)
|
||||
}), {
|
||||
addTask: taskActions.add,
|
||||
updateTask: taskActions.update,
|
||||
removeTask: taskActions.remove
|
||||
})(Todo)
|
50
waspc/examples/todoApp/todoApp.wasp
Normal file
50
waspc/examples/todoApp/todoApp.wasp
Normal file
@ -0,0 +1,50 @@
|
||||
import Todo from "@ext/Todo.js" // Imports non-wasp code from external code dir (ext/).
|
||||
|
||||
entity Task {
|
||||
isDone :: boolean,
|
||||
description :: string
|
||||
}
|
||||
|
||||
app todoApp {
|
||||
title: "ToDo App"
|
||||
}
|
||||
|
||||
page Main {
|
||||
route: "/",
|
||||
style: "@ext/Main.css",
|
||||
// TODO: We need to make this nicer / more explicit, it is not clear where is this coming from (these props).
|
||||
// Also, this wiring is not elegant.
|
||||
// Here we use Todo React component that we imported at the beginning of this file.
|
||||
content: {=jsx <Todo/> jsx=}
|
||||
}
|
||||
|
||||
entity-form<Task> NewTaskForm {
|
||||
fields: {
|
||||
description: {
|
||||
show: true,
|
||||
label: none,
|
||||
placeholder: "What needs to be done?"
|
||||
},
|
||||
isDone: {
|
||||
show: false,
|
||||
defaultValue: false
|
||||
}
|
||||
},
|
||||
submit: {
|
||||
onEnter: true,
|
||||
button: { show: false }
|
||||
}
|
||||
}
|
||||
|
||||
entity-list<Task> TaskList {
|
||||
showHeader: false,
|
||||
fields: {
|
||||
description: {
|
||||
// The contract for render is that user must provide a function that:
|
||||
// - Receives a task as an input.
|
||||
// - Returns a React Node or something that can be rendered by JSX.
|
||||
// - Does not depend on any outer context.
|
||||
render: {=js (task) => task.isDone ? <s>{task.description}</s> : task.description js=}
|
||||
}
|
||||
}
|
||||
}
|
131
waspc/lang-design/todoApp.wasp
Normal file
131
waspc/lang-design/todoApp.wasp
Normal file
@ -0,0 +1,131 @@
|
||||
// Goal of this file is to re-create a TODO app from http://todomvc.com
|
||||
// This file has "advanced" features in the sense that they might not yet be implemented by Wasp: this is just a proposal.
|
||||
|
||||
app todoMVC {
|
||||
title: "ToDo MVC"
|
||||
}
|
||||
|
||||
entity Task {
|
||||
description :: string,
|
||||
isDone :: boolean
|
||||
}
|
||||
|
||||
// IDEA: `@connect Task as taskList` -> this would make it more obvious what is available, also we don't need to automatically try to guess what to import.
|
||||
page Main {
|
||||
route: "/",
|
||||
|
||||
content: {=jsx
|
||||
<div className="mainContainer">
|
||||
<h1>todos</h1>
|
||||
|
||||
<div className="createTaskForm">
|
||||
{/* Toggle all */}
|
||||
<button onClick={() => {
|
||||
const setIsDoneTo = false
|
||||
if (this.props.taskList.some(t => !t.isDone)) setIsDoneTo = true
|
||||
|
||||
this.props.taskList.map(t => this.props.updateTask(t, { isDone: setIsDoneTo }))
|
||||
}}>
|
||||
<Icon id="arrow-down" />
|
||||
</button>
|
||||
|
||||
<CreateTaskForm />
|
||||
</div>
|
||||
|
||||
<div className="taskListContainer">
|
||||
<TaskList filter={this.state.taskFilter} /> { /* Filter here -> that is not supported by TaskList yet. */ }
|
||||
</div>
|
||||
|
||||
<div className="footer">
|
||||
{ /* TODO: This is maybe not very nice, inlined like this.
|
||||
Also, we directly address taskList here, while in TaskList and in CreateTaskForm we address
|
||||
it implicitly. */ }
|
||||
<span>
|
||||
{ this.props.taskList.filter(task => !task.isDone).length } items left
|
||||
</span>
|
||||
{ /* TODO: Can we also make this nicer? */ }
|
||||
<button onClick={() => this.setState({ taskFilter: () => true })}> All </button>
|
||||
<button onClick={() => this.setState({ taskFilter: task => !task.isDone })}> Active </button>
|
||||
<button onClick={() => this.setState({ taskFilter: task => task.isDone })}> Completed </button>
|
||||
|
||||
{/* Clear completed */}
|
||||
{ this.props.taskList.some(t => t.isDone) &&
|
||||
<button onClick={() => { this.state.taskList.map(t => if (t.isDone) this.props.deleteTask(t)) }}>Clear completed</button>
|
||||
}
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
jsx=},
|
||||
|
||||
style: {=css
|
||||
div {
|
||||
color: green;
|
||||
}
|
||||
|
||||
.mainContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.taskListContainer {
|
||||
width: 60%;
|
||||
}
|
||||
css=}
|
||||
}
|
||||
|
||||
// TODO: This part is not currently supported at all.
|
||||
entity-form<Task> CreateTaskForm {
|
||||
fields: {
|
||||
description: {
|
||||
placeholder: "What do you want to do?"
|
||||
},
|
||||
isDone: {
|
||||
show: false,
|
||||
defaultValue: false // Although not shown, this field will be set to "false".
|
||||
}
|
||||
},
|
||||
submit: {
|
||||
button: { show: false },
|
||||
onEnter: true,
|
||||
// Resets fields to their initial values after the submission.
|
||||
resetAllFields: true
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: This part is not currently supported at all.
|
||||
entity-list<Task> TaskList {
|
||||
allowItemEditing: true,
|
||||
allowItemDeletion: true, // Items can be deleted, and this also deletes them for real.
|
||||
|
||||
fields: {
|
||||
description: {
|
||||
// The contract for render is: user can provide a function that:
|
||||
// - receives a task as an input
|
||||
// - returns a React Node or something that can be rendered by JSX
|
||||
// - does not depend on any outer context
|
||||
render: {=js
|
||||
(task) => {
|
||||
if (task.isDone) return (<s>{task.description}</s>)
|
||||
return task.description
|
||||
}
|
||||
js=}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: This part is not currently supported at all.
|
||||
// Idea would be to generate a script (bash) that does deployment and would be called
|
||||
// with `npm deploy`, from generated frontend.
|
||||
// NOTE: For now we don't care about environments (staging) yet, there is just one environment to deploy to (production).
|
||||
deployment {
|
||||
frontend: {
|
||||
// TODO: In case of github, this would go into CNAME file.
|
||||
// NOTE: optional.
|
||||
customDomain: 'todo-app.examples.wasp-lang.dev',
|
||||
github: { // NOTE: For now we allow only one deployment method (in this case "github") at once.
|
||||
branch: 'gh-pages' // NOTE: optional.
|
||||
}
|
||||
}
|
||||
}
|
103
waspc/package.yaml
Normal file
103
waspc/package.yaml
Normal file
@ -0,0 +1,103 @@
|
||||
# This YAML file describes your package. Stack will automatically generate a
|
||||
# Cabal file when you run `stack build`. See the hpack website for help with
|
||||
# this file: <https://github.com/sol/hpack>.
|
||||
name: waspc
|
||||
version: 0.1.0.0
|
||||
github: "Martinsos/waspc"
|
||||
#license: BSD3
|
||||
author: "wasp-lang"
|
||||
maintainer: "sosic.martin@gmail.com, matija.sosic@gmail.com"
|
||||
copyright: "2020 wasp-lang"
|
||||
|
||||
default-extensions:
|
||||
- OverloadedStrings
|
||||
- TemplateHaskell
|
||||
- QuasiQuotes
|
||||
- ScopedTypeVariables
|
||||
|
||||
extra-source-files:
|
||||
- README.md
|
||||
- ChangeLog.md
|
||||
|
||||
data-dir: data/
|
||||
data-files:
|
||||
- Generator/templates/**/*
|
||||
|
||||
# Metadata used when publishing your package
|
||||
# synopsis: Short description of your package
|
||||
# category: Web
|
||||
|
||||
# To avoid duplicated efforts in documentation and dealing with the
|
||||
# complications of embedding Haddock markup inside cabal files, it is
|
||||
# common to point users to the README.md file.
|
||||
description: Please see the README on GitHub at <https://github.com/Martinsos/waspc#readme>
|
||||
|
||||
dependencies:
|
||||
- base >= 4.7 && < 5
|
||||
|
||||
ghc-options:
|
||||
- -Wall
|
||||
|
||||
library:
|
||||
source-dirs: src
|
||||
dependencies:
|
||||
- filepath
|
||||
- parsec
|
||||
- mustache
|
||||
- text
|
||||
- aeson
|
||||
- directory
|
||||
- split
|
||||
- unordered-containers
|
||||
- path
|
||||
- regex-compat
|
||||
- time
|
||||
|
||||
executables:
|
||||
waspc-cli:
|
||||
main: Main.hs
|
||||
source-dirs: app
|
||||
ghc-options:
|
||||
- -threaded
|
||||
- -rtsopts
|
||||
- -with-rtsopts=-N
|
||||
dependencies:
|
||||
- waspc
|
||||
- filepath
|
||||
- path
|
||||
- directory
|
||||
|
||||
benchmarks:
|
||||
waspc-benchmarks:
|
||||
main: Main.hs
|
||||
source-dirs: benchmark
|
||||
ghc-options:
|
||||
- -threaded
|
||||
- -rtsopts
|
||||
- -with-rtsopts=-N
|
||||
dependencies:
|
||||
- waspc
|
||||
- criterion
|
||||
|
||||
tests:
|
||||
waspc-test:
|
||||
main: TastyDiscoverDriver.hs
|
||||
source-dirs: test
|
||||
ghc-options:
|
||||
- -threaded
|
||||
- -rtsopts
|
||||
- -with-rtsopts=-N
|
||||
dependencies:
|
||||
- waspc
|
||||
- tasty
|
||||
- tasty-hspec
|
||||
- tasty-quickcheck
|
||||
- tasty-discover
|
||||
- QuickCheck
|
||||
- aeson
|
||||
- filepath
|
||||
- text
|
||||
- mtl
|
||||
- parsec
|
||||
- deepseq
|
||||
- path
|
85
waspc/run
Executable file
85
waspc/run
Executable file
@ -0,0 +1,85 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# This script defines common commands used during building / developing
|
||||
# and makes it easy to run them.
|
||||
|
||||
THIS=$0
|
||||
COMMAND=${1:-watch}
|
||||
shift
|
||||
ARGS="$@"
|
||||
|
||||
BOLD="\e[1m"
|
||||
UNDERLINED="\e[4m"
|
||||
RESET="\e[0m"
|
||||
LIGHT_CYAN="\e[96m"
|
||||
DEFAULT_COLOR="\e[39m"
|
||||
|
||||
BUILD_CMD="stack build"
|
||||
WATCH_CMD="$BUILD_CMD --file-watch"
|
||||
TEST_CMD="$BUILD_CMD --test"
|
||||
TEST_WATCH_CMD="$TEST_CMD --file-watch"
|
||||
EXEC_CMD="stack exec waspc-cli $ARGS"
|
||||
GHCID_CMD="ghcid --command=stack ghci"
|
||||
|
||||
echo_and_eval () {
|
||||
echo -e $"${LIGHT_CYAN}Running:${DEFAULT_COLOR}" $1 "\n"
|
||||
$1
|
||||
}
|
||||
|
||||
echo_bold () { echo -e $"${BOLD}${1}${RESET}"; }
|
||||
|
||||
print_usage () {
|
||||
print_usage_cmd () {
|
||||
echo -e $" ${UNDERLINED}${1}${RESET}"
|
||||
echo " $2";
|
||||
}
|
||||
|
||||
echo_bold "Usage: ${THIS} <command>"
|
||||
echo "Commands:"
|
||||
print_usage_cmd "build" \
|
||||
"Builds the project."
|
||||
print_usage_cmd "watch" \
|
||||
"Builds the project on any file change. (DEFAULT)"
|
||||
print_usage_cmd "test" \
|
||||
"Builds the project and executes tests."
|
||||
print_usage_cmd "test-watch" \
|
||||
"Builds the project and executes tests on any file change."
|
||||
print_usage_cmd "exec" \
|
||||
"Builds the project once and runs the executable while forwarding arguments."
|
||||
print_usage_cmd "ghcid" \
|
||||
"Runs ghcid, which is much faster alternative to 'watch'."
|
||||
print_usage_cmd "ghcid-test" \
|
||||
"Runs ghcid on tests, which is much faster alternative to 'test-watch'."
|
||||
}
|
||||
|
||||
case $COMMAND in
|
||||
build)
|
||||
echo_and_eval "$BUILD_CMD"
|
||||
;;
|
||||
watch)
|
||||
echo_and_eval "$WATCH_CMD"
|
||||
;;
|
||||
ghcid)
|
||||
echo_and_eval "$GHCID_CMD"
|
||||
;;
|
||||
ghcid-test)
|
||||
# --color always is needed for Tasty to turn on the coloring.
|
||||
# NOTE: I did not put this into variable because I was not able to put single quotes
|
||||
# around :main --color always that way and it was not working.
|
||||
ghcid -T=':main --color always' --command=stack ghci test/TastyDiscoverDriver.hs
|
||||
;;
|
||||
test)
|
||||
echo_and_eval "$TEST_CMD"
|
||||
;;
|
||||
test-watch)
|
||||
echo_and_eval "$TEST_WATCH_CMD"
|
||||
;;
|
||||
exec)
|
||||
echo_and_eval "$BUILD_CMD"
|
||||
echo
|
||||
echo_and_eval "$EXEC_CMD"
|
||||
;;
|
||||
*)
|
||||
print_usage
|
||||
exit 1
|
||||
esac
|
12
waspc/src/CompileOptions.hs
Normal file
12
waspc/src/CompileOptions.hs
Normal file
@ -0,0 +1,12 @@
|
||||
module CompileOptions
|
||||
( CompileOptions(..)
|
||||
) where
|
||||
|
||||
import qualified Path.Aliases as Path
|
||||
|
||||
-- TODO(martin): Should these be merged with Wasp data? Is it really a separate thing or not?
|
||||
-- It would be easier to pass around if it is part of Wasp data. But is it semantically correct?
|
||||
-- Maybe it is, even more than this!
|
||||
data CompileOptions = CompileOptions
|
||||
{ externalCodeDirPath :: !Path.AbsDir
|
||||
}
|
59
waspc/src/ExternalCode.hs
Normal file
59
waspc/src/ExternalCode.hs
Normal file
@ -0,0 +1,59 @@
|
||||
module ExternalCode
|
||||
( File
|
||||
, getFilePathInExtCodeDir
|
||||
, getFileText
|
||||
, readFiles
|
||||
) where
|
||||
|
||||
import qualified Data.Text.Lazy as TextL
|
||||
import qualified Data.Text.Lazy.IO as TextL.IO
|
||||
import Data.Text (Text)
|
||||
import qualified Path
|
||||
import qualified Path.Aliases as Path
|
||||
|
||||
import qualified Util.IO
|
||||
|
||||
|
||||
data File = File
|
||||
{ _pathInExtCodeDir :: !Path.RelFile -- ^ Path relative to external code directory.
|
||||
, _text :: TextL.Text -- ^ File content. It will throw error when evaluated if file is not textual file.
|
||||
}
|
||||
|
||||
instance Show File where
|
||||
show = show . _pathInExtCodeDir
|
||||
|
||||
instance Eq File where
|
||||
f1 == f2 = (_pathInExtCodeDir f1) == (_pathInExtCodeDir f2)
|
||||
|
||||
-- | Returns path relative to the external code directory.
|
||||
getFilePathInExtCodeDir :: File -> Path.RelFile
|
||||
getFilePathInExtCodeDir = _pathInExtCodeDir
|
||||
|
||||
-- | Unsafe method: throws error if text could not be read (if file is not a textual file)!
|
||||
getFileText :: File -> Text
|
||||
getFileText = TextL.toStrict . _text
|
||||
|
||||
|
||||
-- | Returns all files contained in the specified external code dir, recursively.
|
||||
-- File paths are relative to the specified external code dir path.
|
||||
readFiles :: Path.AbsDir -> IO [File]
|
||||
readFiles extCodeDirPath = do
|
||||
relFilePaths <- Util.IO.listDirectoryDeep extCodeDirPath
|
||||
let absFilePaths = map (extCodeDirPath Path.</>) relFilePaths
|
||||
-- NOTE: We read text from all the files, regardless if they are text files or not, because
|
||||
-- we don't know if they are a text file or not.
|
||||
-- Since we do lazy reading (Text.Lazy), this is not a problem as long as we don't try to use
|
||||
-- text of a file that is actually not a text file -> then we will get an error when Haskell
|
||||
-- actually tries to read that file.
|
||||
-- TODO: We are doing lazy IO here, and there is an idea of it being a thing to avoid, due to no
|
||||
-- control over when resources are released and similar.
|
||||
-- If we do figure out that this is causing us problems, we could do the following refactoring:
|
||||
-- Don't read files at this point, just list them, and Wasp will contain just list of filepaths.
|
||||
-- Modify TextFileDraft so that it also takes text transformation function (Text -> Text),
|
||||
-- or create new file draft that will support that.
|
||||
-- In generator, when creating TextFileDraft, give it function/logic for text transformation,
|
||||
-- and it will be taken care of when draft will be written to the disk.
|
||||
fileTexts <- mapM (TextL.IO.readFile . Path.toFilePath) absFilePaths
|
||||
let files = map (\(path, text) -> File path text) (zip relFilePaths fileTexts)
|
||||
return files
|
||||
|
44
waspc/src/Generator.hs
Normal file
44
waspc/src/Generator.hs
Normal file
@ -0,0 +1,44 @@
|
||||
module Generator
|
||||
( writeWebAppCode
|
||||
) where
|
||||
|
||||
import qualified Data.Text
|
||||
import qualified Data.Text.IO
|
||||
import Data.Time.Clock
|
||||
import qualified Paths_waspc
|
||||
import qualified Data.Version
|
||||
import Control.Monad (mapM_)
|
||||
import Path ((</>), relfile)
|
||||
import qualified Path
|
||||
import qualified Path.Aliases as Path
|
||||
|
||||
import CompileOptions (CompileOptions)
|
||||
import Wasp (Wasp)
|
||||
import Generator.Generators (generateWebApp)
|
||||
import Generator.FileDraft (FileDraft, write)
|
||||
|
||||
|
||||
-- | Generates web app code from given Wasp and writes it to given destination directory.
|
||||
-- If dstDir does not exist yet, it will be created.
|
||||
-- NOTE(martin): What if there is already smth in the dstDir? It is probably best
|
||||
-- if we clean it up first? But we don't want this to end up with us deleting stuff
|
||||
-- from user's machine. Maybe we just overwrite and we are good?
|
||||
writeWebAppCode :: Wasp -> Path.AbsDir -> CompileOptions -> IO ()
|
||||
writeWebAppCode wasp dstDir compileOptions = do
|
||||
writeFileDrafts dstDir (generateWebApp wasp compileOptions)
|
||||
writeDotWaspInfo dstDir
|
||||
|
||||
-- | Writes file drafts while using given destination dir as root dir.
|
||||
-- TODO(martin): We could/should parallelize this.
|
||||
-- We could also skip writing files that are already on the disk with same checksum.
|
||||
writeFileDrafts :: Path.AbsDir -> [FileDraft] -> IO ()
|
||||
writeFileDrafts dstDir fileDrafts = mapM_ (write dstDir) fileDrafts
|
||||
|
||||
-- | Writes .waspinfo, which contains some basic metadata about how/when wasp generated the code.
|
||||
writeDotWaspInfo :: Path.AbsDir -> IO ()
|
||||
writeDotWaspInfo dstDir = do
|
||||
currentTime <- getCurrentTime
|
||||
let version = Data.Version.showVersion Paths_waspc.version
|
||||
let content = "Generated on " ++ (show currentTime) ++ " by waspc version " ++ (show version) ++ " ."
|
||||
let dstPath = dstDir </> [relfile|.waspinfo|]
|
||||
Data.Text.IO.writeFile (Path.toFilePath dstPath) (Data.Text.pack content)
|
10
waspc/src/Generator/Common.hs
Normal file
10
waspc/src/Generator/Common.hs
Normal file
@ -0,0 +1,10 @@
|
||||
module Generator.Common
|
||||
( srcDirPath
|
||||
) where
|
||||
|
||||
import Path (reldir)
|
||||
import Path.Aliases as Path
|
||||
|
||||
-- | Path to src directory, relative to the root directory of generated code.
|
||||
srcDirPath :: Path.RelDir
|
||||
srcDirPath = [reldir|src|]
|
104
waspc/src/Generator/Entity.hs
Normal file
104
waspc/src/Generator/Entity.hs
Normal file
@ -0,0 +1,104 @@
|
||||
module Generator.Entity
|
||||
( generateEntities
|
||||
|
||||
, entityDirPathInSrc
|
||||
, entityClassPathInSrc
|
||||
, entityStatePathInSrc
|
||||
, entityActionsPathInSrc
|
||||
|
||||
-- EXPORTED FOR TESTING:
|
||||
, generateEntityClass
|
||||
, generateEntityState
|
||||
, generateEntityActions
|
||||
, generateEntityActionTypes
|
||||
, generateEntityCreateForm
|
||||
|
||||
, entityTemplatesDirPath
|
||||
) where
|
||||
|
||||
import Data.Maybe (fromJust)
|
||||
import Path ((</>), relfile)
|
||||
import qualified Path
|
||||
import qualified Path.Aliases as Path
|
||||
|
||||
import Wasp
|
||||
import Generator.FileDraft
|
||||
import qualified Generator.Common as Common
|
||||
import Generator.Entity.EntityForm (generateEntityCreateForm)
|
||||
import Generator.Entity.EntityList (generateEntityList)
|
||||
import Generator.Entity.Common
|
||||
( entityTemplatesDirPath
|
||||
, entityTemplateData
|
||||
, entityDirPathInSrc
|
||||
)
|
||||
|
||||
|
||||
generateEntities :: Wasp -> [FileDraft]
|
||||
generateEntities wasp = concat $ generateEntity wasp <$> getEntities wasp
|
||||
|
||||
generateEntity :: Wasp -> Entity -> [FileDraft]
|
||||
generateEntity wasp entity =
|
||||
[ generateEntityClass wasp entity
|
||||
, generateEntityState wasp entity
|
||||
, generateEntityActionTypes wasp entity
|
||||
, generateEntityActions wasp entity
|
||||
]
|
||||
++ generateEntityComponents wasp entity
|
||||
|
||||
generateEntityClass :: Wasp -> Entity -> FileDraft
|
||||
generateEntityClass wasp entity
|
||||
= createSimpleEntityFileDraft wasp entity (entityClassPathInSrc entity) [relfile|_Entity.js|]
|
||||
|
||||
generateEntityState :: Wasp -> Entity -> FileDraft
|
||||
generateEntityState wasp entity
|
||||
= createSimpleEntityFileDraft wasp entity (entityStatePathInSrc entity) [relfile|state.js|]
|
||||
|
||||
generateEntityActionTypes :: Wasp -> Entity -> FileDraft
|
||||
generateEntityActionTypes wasp entity
|
||||
= createSimpleEntityFileDraft wasp entity (entityActionTypesPathInSrc entity) [relfile|actionTypes.js|]
|
||||
|
||||
generateEntityActions :: Wasp -> Entity -> FileDraft
|
||||
generateEntityActions wasp entity
|
||||
= createSimpleEntityFileDraft wasp entity (entityActionsPathInSrc entity) [relfile|actions.js|]
|
||||
|
||||
generateEntityComponents :: Wasp -> Entity -> [FileDraft]
|
||||
generateEntityComponents wasp entity = concat
|
||||
[ generateEntityCreateForms wasp entity
|
||||
, generateEntityLists wasp entity
|
||||
]
|
||||
|
||||
-- | Generates creation forms for the given entity.
|
||||
generateEntityCreateForms :: Wasp -> Entity -> [FileDraft]
|
||||
generateEntityCreateForms wasp entity = map (generateEntityCreateForm wasp) entityForms
|
||||
where
|
||||
entityForms = getEntityFormsForEntity wasp entity
|
||||
|
||||
-- | Generates list components for the given entity.
|
||||
generateEntityLists :: Wasp -> Entity -> [FileDraft]
|
||||
generateEntityLists wasp entity = map (generateEntityList wasp) entityLists
|
||||
where
|
||||
entityLists = getEntityListsForEntity wasp entity
|
||||
|
||||
-- | Helper function that captures common logic for generating entity file draft.
|
||||
createSimpleEntityFileDraft :: Wasp -> Entity -> Path.RelFile -> Path.RelFile -> FileDraft
|
||||
createSimpleEntityFileDraft wasp entity dstPathInSrc srcPathInEntityTemplatesDir
|
||||
= createTemplateFileDraft dstPath srcPath (Just templateData)
|
||||
where
|
||||
srcPath = entityTemplatesDirPath </> srcPathInEntityTemplatesDir
|
||||
dstPath = Common.srcDirPath </> dstPathInSrc
|
||||
templateData = entityTemplateData wasp entity
|
||||
|
||||
-- * Paths of generated code (relative to src/ directory)
|
||||
|
||||
entityStatePathInSrc :: Entity -> Path.RelFile
|
||||
entityStatePathInSrc entity = entityDirPathInSrc entity </> [relfile|state.js|]
|
||||
|
||||
entityActionsPathInSrc :: Entity -> Path.RelFile
|
||||
entityActionsPathInSrc entity = entityDirPathInSrc entity </> [relfile|actions.js|]
|
||||
|
||||
entityActionTypesPathInSrc :: Entity -> Path.RelFile
|
||||
entityActionTypesPathInSrc entity = entityDirPathInSrc entity </> [relfile|actionTypes.js|]
|
||||
|
||||
entityClassPathInSrc :: Entity -> Path.RelFile
|
||||
entityClassPathInSrc entity = entityDirPathInSrc entity </>
|
||||
(fromJust $ Path.parseRelFile $ (entityName entity) ++ ".js")
|
75
waspc/src/Generator/Entity/Common.hs
Normal file
75
waspc/src/Generator/Entity/Common.hs
Normal file
@ -0,0 +1,75 @@
|
||||
module Generator.Entity.Common
|
||||
( entityTemplatesDirPath
|
||||
, entityDirPathInSrc
|
||||
, entityTemplateData
|
||||
, entityComponentsDirPathInSrc
|
||||
, entityFieldToJsonWithTypeAsKey
|
||||
, addEntityFieldTypeToJsonAsKeyWithValueTrue
|
||||
, getEntityLowerName
|
||||
, getEntityClassName
|
||||
) where
|
||||
|
||||
import Data.Maybe (fromJust)
|
||||
import Data.Aeson ((.=), object)
|
||||
import qualified Data.Aeson as Aeson
|
||||
import qualified Data.Text as Text
|
||||
import Path ((</>), reldir)
|
||||
import qualified Path
|
||||
import qualified Path.Aliases as Path
|
||||
|
||||
import qualified Util
|
||||
import Wasp
|
||||
|
||||
-- | Path of the entity-related generated code, relative to src/ directory.
|
||||
entityDirPathInSrc :: Entity -> Path.RelDir
|
||||
entityDirPathInSrc entity = [reldir|entities|] </>
|
||||
(fromJust $ Path.parseRelDir $ Util.camelToKebabCase (entityName entity))
|
||||
|
||||
-- | Path of the code generated for entity components, relative to src/ directory.
|
||||
entityComponentsDirPathInSrc :: Entity -> Path.RelDir
|
||||
entityComponentsDirPathInSrc entity = (entityDirPathInSrc entity) </> [reldir|components|]
|
||||
|
||||
-- | Location in templates where entity related templates reside.
|
||||
entityTemplatesDirPath :: Path.RelDir
|
||||
entityTemplatesDirPath = [reldir|src|] </> [reldir|entities|] </> [reldir|_entity|]
|
||||
|
||||
-- | Default generic data for entity templates.
|
||||
entityTemplateData :: Wasp -> Entity -> Aeson.Value
|
||||
entityTemplateData wasp entity = object
|
||||
[ "wasp" .= wasp
|
||||
, "entity" .= entity
|
||||
, "entityLowerName" .= getEntityLowerName entity
|
||||
-- TODO: use it also when creating Class file itself and in other files.
|
||||
, "entityClassName" .= getEntityClassName entity
|
||||
, "entityTypedFields" .= map entityFieldToJsonWithTypeAsKey (entityFields entity)
|
||||
]
|
||||
|
||||
getEntityLowerName :: Entity -> String
|
||||
getEntityLowerName = Util.toLowerFirst . entityName
|
||||
|
||||
getEntityClassName :: Entity -> String
|
||||
getEntityClassName = Util.toUpperFirst . entityName
|
||||
|
||||
{- | Converts entity field to a JSON where field type is a key set to true, along with
|
||||
all other field properties.
|
||||
|
||||
E.g.:
|
||||
boolean field -> { boolean: true, type: "boolean", name: "isDone" }
|
||||
string field -> { string: true, type: "string", name: "description"}
|
||||
|
||||
We need to have "boolean: true" part to achieve conditional rendering with Mustache - in
|
||||
Mustache template we cannot check if type == "boolean", but only whether a "boolean" property
|
||||
is set or not.
|
||||
-}
|
||||
entityFieldToJsonWithTypeAsKey :: EntityField -> Aeson.Value
|
||||
entityFieldToJsonWithTypeAsKey entityField = addEntityFieldTypeToJsonAsKeyWithValueTrue
|
||||
(entityFieldType entityField)
|
||||
(Aeson.toJSON entityField)
|
||||
|
||||
-- | Adds "FIELD_TYPE: true" to a given json. This is needed for Mustache so we can differentiate
|
||||
-- between the form fields of different types.
|
||||
addEntityFieldTypeToJsonAsKeyWithValueTrue :: EntityFieldType -> Aeson.Value -> Aeson.Value
|
||||
addEntityFieldTypeToJsonAsKeyWithValueTrue efType json =
|
||||
Util.jsonSet (toText efType) (Aeson.toJSON True) json
|
||||
where
|
||||
toText = Text.pack . show
|
144
waspc/src/Generator/Entity/EntityForm.hs
Normal file
144
waspc/src/Generator/Entity/EntityForm.hs
Normal file
@ -0,0 +1,144 @@
|
||||
module Generator.Entity.EntityForm
|
||||
( generateEntityCreateForm
|
||||
|
||||
-- For testing
|
||||
, entityCreateFormPathInSrc
|
||||
, FormFieldTemplateData(..)
|
||||
) where
|
||||
|
||||
import Control.Exception (assert)
|
||||
import Data.Aeson ((.=), object, ToJSON(..))
|
||||
import Data.Maybe (fromJust)
|
||||
import Path ((</>), reldir, relfile)
|
||||
import qualified Path
|
||||
import qualified Path.Aliases as Path
|
||||
import qualified Util
|
||||
|
||||
import qualified Wasp as Wasp
|
||||
import Wasp (Wasp)
|
||||
|
||||
import qualified Wasp.EntityForm as WEF
|
||||
import qualified Generator.FileDraft as FD
|
||||
import qualified Generator.Entity.Common as EC
|
||||
import qualified Generator.Common as Common
|
||||
|
||||
-- | Data which will be fed to the Mustache "create form" template.
|
||||
data EntityFormTemplateData = EntityFormTemplateData
|
||||
{ _formName :: !String
|
||||
, _entityClassName :: !String
|
||||
, _formFields :: ![FormFieldTemplateData]
|
||||
-- Submit
|
||||
, _showSubmitButton :: !Bool
|
||||
} deriving (Show)
|
||||
|
||||
instance ToJSON EntityFormTemplateData where
|
||||
toJSON td = object
|
||||
[ "formName" .= _formName td
|
||||
, "entityClassName" .= _entityClassName td
|
||||
, "formFields" .= _formFields td
|
||||
, "showSubmitButton" .= _showSubmitButton td
|
||||
]
|
||||
|
||||
-- | Represents template data for the individual form field.
|
||||
data FormFieldTemplateData = FormFieldTemplateData
|
||||
{ _fieldName :: !String
|
||||
, _fieldType :: !Wasp.EntityFieldType
|
||||
, _fieldShow :: !Bool
|
||||
, _fieldDefaultValue :: !WEF.DefaultValue
|
||||
, _fieldPlaceholder :: Maybe String
|
||||
, _fieldLabel :: Maybe String
|
||||
} deriving (Show)
|
||||
|
||||
instance ToJSON FormFieldTemplateData where
|
||||
toJSON f = EC.addEntityFieldTypeToJsonAsKeyWithValueTrue (_fieldType f) $
|
||||
object
|
||||
[ "name" .= _fieldName f
|
||||
, "type" .= _fieldType f
|
||||
, "show" .= _fieldShow f
|
||||
, "defaultValue" .= case (_fieldDefaultValue f) of
|
||||
(WEF.DefaultValueString s) -> s
|
||||
(WEF.DefaultValueBool b) -> Util.toLowerFirst $ show b
|
||||
, "placeholder" .= _fieldPlaceholder f
|
||||
, "label" .= _fieldLabel f
|
||||
]
|
||||
|
||||
-- | Given entity and an entity form for it, creates a single data structure
|
||||
-- with all the values needed by the template to generate the form.
|
||||
createEntityFormTemplateData :: Wasp.Entity -> WEF.EntityForm -> EntityFormTemplateData
|
||||
createEntityFormTemplateData entity entityForm =
|
||||
assert (Wasp.entityName entity == WEF._entityName entityForm) $
|
||||
|
||||
EntityFormTemplateData
|
||||
{ _formName = WEF._name entityForm
|
||||
, _entityClassName = EC.getEntityClassName entity
|
||||
, _formFields = map (createFormFieldTD entityForm) $ Wasp.entityFields entity
|
||||
-- Submit
|
||||
, _showSubmitButton = maybe True id maybeShowSubmitButton
|
||||
}
|
||||
where
|
||||
maybeShowSubmitButton :: Maybe Bool
|
||||
maybeShowSubmitButton = WEF._submit entityForm >>= WEF._submitButton >>= WEF._submitButtonShow
|
||||
|
||||
-- | Given field data from entity and an entity form for it, creates a single
|
||||
-- data structure with all the values needed by the template to generate a form field.
|
||||
createFormFieldTD :: WEF.EntityForm -> Wasp.EntityField -> FormFieldTemplateData
|
||||
createFormFieldTD entityForm entityField = FormFieldTemplateData
|
||||
{ _fieldName = Wasp.entityFieldName entityField
|
||||
, _fieldType = Wasp.entityFieldType entityField
|
||||
, _fieldShow = maybe True id $ formFieldConfig >>= WEF._fieldShow
|
||||
, _fieldDefaultValue = maybe
|
||||
defaultValueIfNothingInForm
|
||||
id
|
||||
$ formFieldConfig >>= WEF._fieldDefaultValue
|
||||
, _fieldPlaceholder = formFieldConfig >>= WEF._fieldPlaceholder
|
||||
, _fieldLabel = fieldLabel
|
||||
}
|
||||
where
|
||||
-- Configuration of a form field within entity-form, if there is any.
|
||||
formFieldConfig :: Maybe WEF.Field
|
||||
formFieldConfig = WEF.getConfigForField entityForm entityField
|
||||
|
||||
getDefaultValueForFieldWithType :: Wasp.EntityFieldType -> WEF.DefaultValue
|
||||
getDefaultValueForFieldWithType efType = case efType of
|
||||
Wasp.EftString -> WEF.DefaultValueString ""
|
||||
Wasp.EftBoolean -> WEF.DefaultValueBool False
|
||||
|
||||
-- If user did not explicitly set a default value, we determine it ourselves.
|
||||
defaultValueIfNothingInForm :: WEF.DefaultValue
|
||||
defaultValueIfNothingInForm =
|
||||
getDefaultValueForFieldWithType $ Wasp.entityFieldType entityField
|
||||
|
||||
fieldLabel :: Maybe String
|
||||
fieldLabel = case (formFieldConfig >>= WEF._fieldLabel) of
|
||||
-- Label property is not provided -> in that case we set label to the
|
||||
-- default value of entity field name (e.g. "description").
|
||||
Nothing -> Just $ Wasp.entityFieldName entityField
|
||||
-- Label property is provided and explicitly disabled ('label: none').
|
||||
Just Nothing -> Nothing
|
||||
-- Label property is provided and set to a specific value ('label: "something"').
|
||||
Just (Just label) -> Just label
|
||||
|
||||
|
||||
-- | Generates entity creation form.
|
||||
generateEntityCreateForm :: Wasp -> WEF.EntityForm -> FD.FileDraft
|
||||
generateEntityCreateForm wasp entityForm =
|
||||
FD.createTemplateFileDraft dstPath templateSrcPath (Just templateData)
|
||||
where
|
||||
-- NOTE(matija): There should always be an entity in wasp for the given entity form.
|
||||
-- If not, we want an error to be thrown.
|
||||
entity = maybe
|
||||
(error $ "Wasp must contain entity to which the entity form refers: " ++
|
||||
WEF._entityName entityForm)
|
||||
id
|
||||
(Wasp.getEntityByName wasp (WEF._entityName entityForm))
|
||||
|
||||
templateSrcPath = EC.entityTemplatesDirPath </> [reldir|components|] </> [relfile|CreateForm.js|]
|
||||
|
||||
dstPath = Common.srcDirPath </> (entityCreateFormPathInSrc entity entityForm)
|
||||
|
||||
templateData = toJSON $ createEntityFormTemplateData entity entityForm
|
||||
|
||||
entityCreateFormPathInSrc :: Wasp.Entity -> WEF.EntityForm -> Path.RelFile
|
||||
entityCreateFormPathInSrc entity entityForm =
|
||||
EC.entityComponentsDirPathInSrc entity </>
|
||||
(fromJust $ Path.parseRelFile $ (WEF._name entityForm) ++ ".js")
|
134
waspc/src/Generator/Entity/EntityList.hs
Normal file
134
waspc/src/Generator/Entity/EntityList.hs
Normal file
@ -0,0 +1,134 @@
|
||||
module Generator.Entity.EntityList
|
||||
( generateEntityList
|
||||
, entityListPathInSrc
|
||||
) where
|
||||
|
||||
import Control.Exception (assert)
|
||||
import Data.Aeson ((.=), object, ToJSON(..))
|
||||
import Data.Maybe (fromJust)
|
||||
import Path ((</>), reldir, relfile, parseRelFile)
|
||||
import qualified Path.Aliases as Path
|
||||
|
||||
import qualified Util as U
|
||||
|
||||
import qualified Wasp
|
||||
import Wasp (Wasp)
|
||||
import qualified Wasp.EntityList as WEL
|
||||
import qualified Wasp.JsCode
|
||||
|
||||
import qualified Generator.FileDraft as FD
|
||||
import qualified Generator.Entity.Common as EC
|
||||
import qualified Generator.Common as Common
|
||||
|
||||
|
||||
data EntityListTemplateData = EntityListTemplateData
|
||||
{ _listName :: !String
|
||||
, _entityName :: !String
|
||||
, _entityClassName :: !String
|
||||
, _entityLowerName :: !String
|
||||
, _listShowHeader :: !Bool
|
||||
, _listFields :: ![ListFieldTemplateData]
|
||||
|
||||
, _entityBeingEditedStateVar :: !String
|
||||
}
|
||||
|
||||
instance ToJSON EntityListTemplateData where
|
||||
toJSON td = object
|
||||
[ "listName" .= _listName td
|
||||
, "entityName" .= _entityName td
|
||||
, "entityClassName" .= _entityClassName td
|
||||
, "entityLowerName" .= _entityLowerName td
|
||||
, "showHeader" .= _listShowHeader td
|
||||
, "listFields" .= _listFields td
|
||||
, "entityBeingEditedStateVar" .= _entityBeingEditedStateVar td
|
||||
]
|
||||
|
||||
data ListFieldTemplateData = ListFieldTemplateData
|
||||
{ _fieldName :: !String
|
||||
, _fieldType :: !Wasp.EntityFieldType
|
||||
, _fieldWidthAsPercent :: !Int
|
||||
-- Render
|
||||
, _fieldRender :: Maybe Wasp.JsCode.JsCode
|
||||
, _fieldRenderFnName :: String
|
||||
}
|
||||
|
||||
instance ToJSON ListFieldTemplateData where
|
||||
toJSON f = EC.addEntityFieldTypeToJsonAsKeyWithValueTrue (_fieldType f) $
|
||||
object
|
||||
[ "name" .= _fieldName f
|
||||
, "type" .= _fieldType f
|
||||
, "widthAsPercent" .= _fieldWidthAsPercent f
|
||||
, "render" .= _fieldRender f
|
||||
, "renderFnName" .= _fieldRenderFnName f
|
||||
]
|
||||
|
||||
createEntityListTemplateData :: Wasp.Entity -> WEL.EntityList -> EntityListTemplateData
|
||||
createEntityListTemplateData entity entityList =
|
||||
assert (Wasp.entityName entity == WEL._entityName entityList) $
|
||||
|
||||
EntityListTemplateData
|
||||
{ _listName = WEL._name entityList
|
||||
, _entityName = Wasp.entityName entity
|
||||
, _entityClassName = EC.getEntityClassName entity
|
||||
, _entityLowerName = EC.getEntityLowerName entity
|
||||
, _listShowHeader = showHeader
|
||||
, _listFields = map (createListFieldTD entity entityList) $ Wasp.entityFields entity
|
||||
, _entityBeingEditedStateVar = entityLowerName ++ "BeingEdited"
|
||||
}
|
||||
where
|
||||
entityLowerName = EC.getEntityLowerName entity
|
||||
|
||||
showHeader = maybe True id (WEL._showHeader entityList)
|
||||
|
||||
createListFieldTD :: Wasp.Entity -> WEL.EntityList -> Wasp.EntityField -> ListFieldTemplateData
|
||||
createListFieldTD entity entityList entityField = ListFieldTemplateData
|
||||
{ _fieldName = Wasp.entityFieldName entityField
|
||||
, _fieldType = Wasp.entityFieldType entityField
|
||||
, _fieldWidthAsPercent = fieldWidthAsPercent
|
||||
, _fieldRender = listFieldConfig >>= WEL._fieldRender
|
||||
, _fieldRenderFnName = "render" ++ entityUpper ++ entityFieldUpper
|
||||
}
|
||||
where
|
||||
-- Configuration of a form field within entity-list, if there is any.
|
||||
listFieldConfig :: Maybe WEL.Field
|
||||
listFieldConfig = WEL.getConfigForField entityList entityField
|
||||
|
||||
entityUpper = U.toUpperFirst $ Wasp.entityName entity
|
||||
entityFieldUpper = U.toUpperFirst $ Wasp.entityFieldName entityField
|
||||
|
||||
-- NOTE(matija): for now we decide on width, it is not yet an option exposed to the
|
||||
-- user. Our current strategy, since we have no outside info, is to make all fields
|
||||
-- have approx. equal width.
|
||||
--
|
||||
-- Since we use Int, obviously we will not be able to always get a sum of 100. But with
|
||||
-- the current style framework we are using (material-ui), it is not a problem.
|
||||
--
|
||||
-- E.g. if we have 3 fields, each of them will get assigned a width of 33 - material-ui
|
||||
-- will then resolve that by itself - it will assign the missing width to one of the
|
||||
-- fields (the last one, from what I tried).
|
||||
fieldWidthAsPercent :: Int
|
||||
fieldWidthAsPercent = 100 `div` (length $ Wasp.entityFields entity)
|
||||
|
||||
generateEntityList :: Wasp -> WEL.EntityList -> FD.FileDraft
|
||||
generateEntityList wasp entityList =
|
||||
FD.createTemplateFileDraft dstPath templateSrcPath (Just templateData)
|
||||
where
|
||||
-- NOTE(matija): There should always be an entity in wasp for the given entity list.
|
||||
-- If not, we want an error to be thrown.
|
||||
entity = maybe
|
||||
(error $ "Wasp must contain entity to which the entity list refers: " ++
|
||||
WEL._entityName entityList)
|
||||
id
|
||||
(Wasp.getEntityByName wasp (WEL._entityName entityList))
|
||||
|
||||
templateSrcPath = EC.entityTemplatesDirPath </> [reldir|components|] </> [relfile|List.js|]
|
||||
|
||||
dstPath = Common.srcDirPath </> (entityListPathInSrc entity entityList)
|
||||
|
||||
templateData = toJSON $ createEntityListTemplateData entity entityList
|
||||
|
||||
-- | Path in the generated src dir where the given entity list will be located.
|
||||
entityListPathInSrc :: Wasp.Entity -> WEL.EntityList -> Path.RelFile
|
||||
entityListPathInSrc entity entityList =
|
||||
EC.entityComponentsDirPathInSrc entity </>
|
||||
(fromJust $ parseRelFile $ (WEL._name entityList) ++ ".js")
|
30
waspc/src/Generator/ExternalCode.hs
Normal file
30
waspc/src/Generator/ExternalCode.hs
Normal file
@ -0,0 +1,30 @@
|
||||
module Generator.ExternalCode
|
||||
( generateExternalCodeDir
|
||||
) where
|
||||
|
||||
import qualified System.FilePath as FP
|
||||
import qualified Path
|
||||
|
||||
import CompileOptions (CompileOptions)
|
||||
import Wasp (Wasp)
|
||||
import qualified Wasp
|
||||
import qualified ExternalCode
|
||||
import qualified Generator.FileDraft as FD
|
||||
import qualified Generator.ExternalCode.Common as Common
|
||||
import Generator.ExternalCode.Js (generateJsFile)
|
||||
|
||||
|
||||
generateExternalCodeDir :: CompileOptions -> Wasp -> [FD.FileDraft]
|
||||
generateExternalCodeDir compileOptions wasp =
|
||||
map (generateFile compileOptions) (Wasp.getExternalCodeFiles wasp)
|
||||
|
||||
generateFile :: CompileOptions -> ExternalCode.File -> FD.FileDraft
|
||||
generateFile compileOptions file
|
||||
| extension `elem` [".js", ".jsx"] = generateJsFile file
|
||||
| otherwise = FD.createCopyFileDraft (Common.getExtCodeFileDstPath file)
|
||||
(Common.getExtCodeFileSrcPath compileOptions file)
|
||||
where
|
||||
extension = FP.takeExtension $ Path.toFilePath $ Common.getExtCodeFileSrcPath compileOptions file
|
||||
|
||||
|
||||
|
31
waspc/src/Generator/ExternalCode/Common.hs
Normal file
31
waspc/src/Generator/ExternalCode/Common.hs
Normal file
@ -0,0 +1,31 @@
|
||||
module Generator.ExternalCode.Common
|
||||
( externalCodeDirPathInSrc
|
||||
, getExtCodeFileDstPath
|
||||
, getExtCodeFileSrcPath
|
||||
) where
|
||||
|
||||
import Path ((</>), reldir)
|
||||
import qualified Path.Aliases as Path
|
||||
|
||||
import CompileOptions (CompileOptions)
|
||||
import qualified CompileOptions
|
||||
import qualified ExternalCode
|
||||
import qualified Generator.Common as Common
|
||||
|
||||
|
||||
externalCodeDirPathInSrc :: Path.RelDir
|
||||
externalCodeDirPathInSrc = [reldir|ext-src|]
|
||||
|
||||
-- | Returns path where external code file will be generated,
|
||||
-- relative to the root of the generated project.
|
||||
getExtCodeFileDstPath :: ExternalCode.File -> Path.RelFile
|
||||
getExtCodeFileDstPath file = Common.srcDirPath </> externalCodeDirPathInSrc </>
|
||||
ExternalCode.getFilePathInExtCodeDir file
|
||||
|
||||
-- | Returns absolute path of the original external code file.
|
||||
getExtCodeFileSrcPath :: CompileOptions -> ExternalCode.File -> Path.AbsFile
|
||||
getExtCodeFileSrcPath compileOptions file = CompileOptions.externalCodeDirPath compileOptions </>
|
||||
ExternalCode.getFilePathInExtCodeDir file
|
||||
|
||||
|
||||
|
44
waspc/src/Generator/ExternalCode/Js.hs
Normal file
44
waspc/src/Generator/ExternalCode/Js.hs
Normal file
@ -0,0 +1,44 @@
|
||||
module Generator.ExternalCode.Js
|
||||
( generateJsFile
|
||||
-- FOR TESTING:
|
||||
, resolveJsFileWaspImports
|
||||
) where
|
||||
|
||||
import qualified Text.Regex as TR
|
||||
import Data.Text (Text, pack, unpack)
|
||||
import Path ((</>))
|
||||
import qualified Path
|
||||
import qualified Path.Aliases as Path
|
||||
import qualified Path.Extra as Path
|
||||
|
||||
import qualified Generator.FileDraft as FD
|
||||
import qualified ExternalCode
|
||||
import qualified Generator.ExternalCode.Common as Common
|
||||
|
||||
|
||||
generateJsFile :: ExternalCode.File -> FD.FileDraft
|
||||
generateJsFile file = FD.createTextFileDraft (Common.getExtCodeFileDstPath file) text'
|
||||
where
|
||||
text = ExternalCode.getFileText file
|
||||
text' = resolveJsFileWaspImports jsFilePathInSrcDir text
|
||||
jsFilePathInSrcDir = Common.externalCodeDirPathInSrc </>
|
||||
ExternalCode.getFilePathInExtCodeDir file
|
||||
|
||||
|
||||
-- | Takes a file path where the external code js file will be generated, relative to generated src dir.
|
||||
-- Also takes text of the file. Returns text where special @wasp imports have been replaced with
|
||||
-- imports that will work.
|
||||
-- TODO: I had hard time finding popular libraries for more advanced replacements, so for now
|
||||
-- I just used very simple regex replacement, which might not work in some complicated situations
|
||||
-- (it will also match on commens and strings and similar).
|
||||
-- For the future, we should probably use some kind of better regex or even some kind of parser.
|
||||
-- Possible candidates: replace-attoparsec.
|
||||
resolveJsFileWaspImports :: Path.RelFile -> Text -> Text
|
||||
resolveJsFileWaspImports jsFilePathInSrcDir jsFileText = pack $
|
||||
-- NOTE(matija): we could not use "\\s+" in the regex below because it does not
|
||||
-- work on OS X for some unknown reason. This is why we use " +" instead.
|
||||
-- Maybe we should user another regex library, e.g. regexec from
|
||||
-- https://hackage.haskell.org/package/regex-posix-0.96.0.0/docs/Text-Regex-Posix-String.html
|
||||
TR.subRegex (TR.mkRegex "(from +['\"])@wasp/")
|
||||
(unpack jsFileText)
|
||||
("\\1" ++ Path.reversePath (Path.parent jsFilePathInSrcDir) ++ "/")
|
53
waspc/src/Generator/FileDraft.hs
Normal file
53
waspc/src/Generator/FileDraft.hs
Normal file
@ -0,0 +1,53 @@
|
||||
module Generator.FileDraft
|
||||
( FileDraft(..)
|
||||
, Writeable(..)
|
||||
, createTemplateFileDraft
|
||||
, createCopyFileDraft
|
||||
, createTextFileDraft
|
||||
) where
|
||||
|
||||
import qualified Data.Aeson as Aeson
|
||||
import Data.Text (Text)
|
||||
import qualified Path.Aliases as Path
|
||||
|
||||
import Generator.FileDraft.Writeable
|
||||
|
||||
import Generator.FileDraft.TemplateFileDraft (TemplateFileDraft)
|
||||
import qualified Generator.FileDraft.TemplateFileDraft as TmplFD
|
||||
|
||||
import Generator.FileDraft.CopyFileDraft (CopyFileDraft)
|
||||
import qualified Generator.FileDraft.CopyFileDraft as CopyFD
|
||||
|
||||
import Generator.FileDraft.TextFileDraft (TextFileDraft)
|
||||
import qualified Generator.FileDraft.TextFileDraft as TextFD
|
||||
|
||||
|
||||
-- | FileDraft unites different file draft types into a single type,
|
||||
-- so that in the rest of the system they can be passed around as heterogeneous
|
||||
-- collection when needed.
|
||||
data FileDraft
|
||||
= FileDraftTemplateFd TemplateFileDraft
|
||||
| FileDraftCopyFd CopyFileDraft
|
||||
| FileDraftTextFd TextFileDraft
|
||||
deriving (Show, Eq)
|
||||
|
||||
instance Writeable FileDraft where
|
||||
write dstDir (FileDraftTemplateFd draft) = write dstDir draft
|
||||
write dstDir (FileDraftCopyFd draft) = write dstDir draft
|
||||
write dstDir (FileDraftTextFd draft) = write dstDir draft
|
||||
|
||||
|
||||
createTemplateFileDraft :: Path.RelFile -> Path.RelFile -> Maybe Aeson.Value -> FileDraft
|
||||
createTemplateFileDraft dstPath tmplSrcPath tmplData =
|
||||
FileDraftTemplateFd $ TmplFD.TemplateFileDraft { TmplFD._dstPath = dstPath
|
||||
, TmplFD._srcPathInTmplDir = tmplSrcPath
|
||||
, TmplFD._tmplData = tmplData
|
||||
}
|
||||
|
||||
createCopyFileDraft :: Path.RelFile -> Path.AbsFile -> FileDraft
|
||||
createCopyFileDraft dstPath srcPath =
|
||||
FileDraftCopyFd $ CopyFD.CopyFileDraft { CopyFD._dstPath = dstPath, CopyFD._srcPath = srcPath}
|
||||
|
||||
createTextFileDraft :: Path.RelFile -> Text -> FileDraft
|
||||
createTextFileDraft dstPath content =
|
||||
FileDraftTextFd $ TextFD.TextFileDraft { TextFD._dstPath = dstPath, TextFD._content = content}
|
24
waspc/src/Generator/FileDraft/CopyFileDraft.hs
Normal file
24
waspc/src/Generator/FileDraft/CopyFileDraft.hs
Normal file
@ -0,0 +1,24 @@
|
||||
module Generator.FileDraft.CopyFileDraft
|
||||
( CopyFileDraft(..)
|
||||
) where
|
||||
|
||||
import qualified Path
|
||||
import qualified Path.Aliases as Path
|
||||
|
||||
import Generator.FileDraft.Writeable
|
||||
import Generator.FileDraft.WriteableMonad
|
||||
|
||||
|
||||
-- | File draft based purely on another file, that is just copied.
|
||||
data CopyFileDraft = CopyFileDraft
|
||||
{ _dstPath :: !Path.RelFile -- ^ Path of file to be written, relative to some root dir.
|
||||
, _srcPath :: !Path.AbsFile -- ^ Absolute path of source file to copy.
|
||||
}
|
||||
deriving (Show, Eq)
|
||||
|
||||
instance Writeable CopyFileDraft where
|
||||
write absDstDirPath draft = do
|
||||
createDirectoryIfMissing True (Path.toFilePath $ Path.parent absDraftDstPath)
|
||||
copyFile (Path.toFilePath $ _srcPath draft) (Path.toFilePath $ absDraftDstPath)
|
||||
where
|
||||
absDraftDstPath = absDstDirPath Path.</> (_dstPath draft)
|
33
waspc/src/Generator/FileDraft/TemplateFileDraft.hs
Normal file
33
waspc/src/Generator/FileDraft/TemplateFileDraft.hs
Normal file
@ -0,0 +1,33 @@
|
||||
module Generator.FileDraft.TemplateFileDraft
|
||||
( TemplateFileDraft(..)
|
||||
) where
|
||||
|
||||
import qualified Data.Aeson as Aeson
|
||||
import qualified Path
|
||||
import qualified Path.Aliases as Path
|
||||
|
||||
import Generator.FileDraft.Writeable
|
||||
import Generator.FileDraft.WriteableMonad
|
||||
|
||||
|
||||
-- | File draft based on template file that gets combined with data.
|
||||
data TemplateFileDraft = TemplateFileDraft
|
||||
{ _dstPath :: !Path.RelFile -- ^ Path of file to be written, relative to some dst root dir.
|
||||
, _srcPathInTmplDir :: !Path.RelFile -- ^ Path of template source file, relative to templates root dir.
|
||||
, _tmplData :: Maybe Aeson.Value -- ^ Data to be fed to the template while rendering it.
|
||||
}
|
||||
deriving (Show, Eq)
|
||||
|
||||
instance Writeable TemplateFileDraft where
|
||||
write absDstDirPath draft = do
|
||||
createDirectoryIfMissing True (Path.toFilePath $ Path.parent absDraftDstPath)
|
||||
case _tmplData draft of
|
||||
Nothing -> do
|
||||
absDraftSrcPath <- getTemplateFileAbsPath (_srcPathInTmplDir draft)
|
||||
copyFile (Path.toFilePath absDraftSrcPath) (Path.toFilePath absDraftDstPath)
|
||||
Just tmplData -> do
|
||||
content <- compileAndRenderTemplate (_srcPathInTmplDir draft) tmplData
|
||||
writeFileFromText (Path.toFilePath absDraftDstPath) content
|
||||
where
|
||||
absDraftDstPath :: Path.AbsFile
|
||||
absDraftDstPath = absDstDirPath Path.</> (_dstPath draft)
|
26
waspc/src/Generator/FileDraft/TextFileDraft.hs
Normal file
26
waspc/src/Generator/FileDraft/TextFileDraft.hs
Normal file
@ -0,0 +1,26 @@
|
||||
module Generator.FileDraft.TextFileDraft
|
||||
( TextFileDraft(..)
|
||||
) where
|
||||
|
||||
import qualified Path
|
||||
import qualified Path.Aliases as Path
|
||||
|
||||
import Generator.FileDraft.Writeable
|
||||
import Generator.FileDraft.WriteableMonad
|
||||
|
||||
import Data.Text (Text)
|
||||
|
||||
|
||||
-- | File draft based on text, that is to be written to file when time comes.
|
||||
data TextFileDraft = TextFileDraft
|
||||
{ _dstPath :: !Path.RelFile -- ^ Path of file to be written, relative to some root dir.
|
||||
, _content :: Text
|
||||
}
|
||||
deriving (Show, Eq)
|
||||
|
||||
instance Writeable TextFileDraft where
|
||||
write dstDir draft = do
|
||||
createDirectoryIfMissing True (Path.toFilePath $ Path.parent absDraftDstPath)
|
||||
writeFileFromText (Path.toFilePath absDraftDstPath) (_content draft)
|
||||
where
|
||||
absDraftDstPath = dstDir Path.</> (_dstPath draft)
|
14
waspc/src/Generator/FileDraft/Writeable.hs
Normal file
14
waspc/src/Generator/FileDraft/Writeable.hs
Normal file
@ -0,0 +1,14 @@
|
||||
module Generator.FileDraft.Writeable
|
||||
( Writeable(..)
|
||||
) where
|
||||
|
||||
import qualified Path.Aliases as Path
|
||||
|
||||
import Generator.FileDraft.WriteableMonad
|
||||
|
||||
class Writeable w where
|
||||
-- | Write file somewhere in the provided dst directory.
|
||||
write :: (WriteableMonad m)
|
||||
=> Path.AbsDir -- ^ Absolute path of dst directory.
|
||||
-> w
|
||||
-> m ()
|
56
waspc/src/Generator/FileDraft/WriteableMonad.hs
Normal file
56
waspc/src/Generator/FileDraft/WriteableMonad.hs
Normal file
@ -0,0 +1,56 @@
|
||||
module Generator.FileDraft.WriteableMonad
|
||||
( WriteableMonad(..)
|
||||
) where
|
||||
|
||||
|
||||
import qualified System.Directory
|
||||
import qualified Data.Text.IO
|
||||
import Data.Aeson as Aeson
|
||||
import Data.Text (Text)
|
||||
import qualified Path.Aliases as Path
|
||||
|
||||
import qualified Generator.Templates
|
||||
|
||||
|
||||
-- TODO: Should we use DI via data instead of typeclasses?
|
||||
-- https://news.ycombinator.com/item?id=10392044
|
||||
|
||||
-- TODO: Should we make constraint MonadIO instead of just Monad?
|
||||
-- That would allow us to do liftIO. And that might allow us to perform any IO
|
||||
-- we want (hm will it?), which could be useful for custom stuff (but does that defeat the whole purpose?).
|
||||
-- But that means we can't test that part, which yes, defeats the purpose somewhat.
|
||||
-- I feel like all together we should not do it :), but it is an option if needed.
|
||||
|
||||
-- | Describes effects needed by File Drafts.
|
||||
class (Monad m) => WriteableMonad m where
|
||||
createDirectoryIfMissing
|
||||
:: Bool -- ^ True if parents should also be created.
|
||||
-> FilePath -- ^ Path to the directory to create.
|
||||
-> m ()
|
||||
|
||||
copyFile
|
||||
:: FilePath -- ^ Src path.
|
||||
-> FilePath -- ^ Dst path.
|
||||
-> m ()
|
||||
|
||||
writeFileFromText :: FilePath -> Text -> m ()
|
||||
|
||||
getTemplateFileAbsPath
|
||||
:: Path.RelFile -- ^ Template file path, relative to templates root directory.
|
||||
-> m Path.AbsFile
|
||||
|
||||
-- | Returns absolute path of templates root directory.
|
||||
getTemplatesDirAbsPath :: m Path.AbsDir
|
||||
|
||||
compileAndRenderTemplate
|
||||
:: Path.RelFile -- ^ Path to the template file, relative to template root dir.
|
||||
-> Aeson.Value -- ^ JSON to be provided as template data.
|
||||
-> m Text
|
||||
|
||||
instance WriteableMonad IO where
|
||||
createDirectoryIfMissing = System.Directory.createDirectoryIfMissing
|
||||
copyFile = System.Directory.copyFile
|
||||
writeFileFromText = Data.Text.IO.writeFile
|
||||
getTemplateFileAbsPath = Generator.Templates.getTemplateFileAbsPath
|
||||
getTemplatesDirAbsPath = Generator.Templates.getTemplatesDirAbsPath
|
||||
compileAndRenderTemplate = Generator.Templates.compileAndRenderTemplate
|
88
waspc/src/Generator/Generators.hs
Normal file
88
waspc/src/Generator/Generators.hs
Normal file
@ -0,0 +1,88 @@
|
||||
module Generator.Generators
|
||||
( generateWebApp
|
||||
) where
|
||||
|
||||
import Data.Aeson ((.=), object, ToJSON(..))
|
||||
import Path ((</>), reldir, relfile)
|
||||
import qualified Path
|
||||
import qualified Path.Aliases as Path
|
||||
|
||||
import CompileOptions (CompileOptions)
|
||||
import qualified Util
|
||||
import Wasp
|
||||
import Generator.FileDraft
|
||||
import qualified Generator.Entity as EntityGenerator
|
||||
import qualified Generator.PageGenerator as PageGenerator
|
||||
import qualified Generator.ExternalCode as ExternalCodeGenerator
|
||||
import qualified Generator.Common as Common
|
||||
|
||||
|
||||
generateWebApp :: Wasp -> CompileOptions -> [FileDraft]
|
||||
generateWebApp wasp options = concatMap ($ wasp)
|
||||
[ generateReadme
|
||||
, generatePackageJson
|
||||
, generateGitignore
|
||||
, generatePublicDir
|
||||
, generateSrcDir
|
||||
, ExternalCodeGenerator.generateExternalCodeDir options
|
||||
]
|
||||
|
||||
generateReadme :: Wasp -> [FileDraft]
|
||||
generateReadme wasp = [simpleTemplateFileDraft [relfile|README.md|] wasp]
|
||||
|
||||
generatePackageJson :: Wasp -> [FileDraft]
|
||||
generatePackageJson wasp = [simpleTemplateFileDraft [relfile|package.json|] wasp]
|
||||
|
||||
generateGitignore :: Wasp -> [FileDraft]
|
||||
generateGitignore wasp = [createTemplateFileDraft [relfile|.gitignore|] [relfile|gitignore|] (Just $ toJSON wasp)]
|
||||
|
||||
generatePublicDir :: Wasp -> [FileDraft]
|
||||
generatePublicDir wasp =
|
||||
createTemplateFileDraft ([reldir|public|] </> [relfile|favicon.ico|])
|
||||
([reldir|public|] </> [relfile|favicon.ico|])
|
||||
Nothing
|
||||
: map (\path -> simpleTemplateFileDraft ([reldir|public|] </> path) wasp)
|
||||
[ [relfile|index.html|]
|
||||
, [relfile|manifest.json|]
|
||||
]
|
||||
|
||||
-- * Src dir
|
||||
|
||||
generateSrcDir :: Wasp -> [FileDraft]
|
||||
generateSrcDir wasp
|
||||
= (createTemplateFileDraft (Common.srcDirPath </> [relfile|logo.png|])
|
||||
([reldir|src|] </> [relfile|logo.png|])
|
||||
Nothing)
|
||||
: map (\path -> simpleTemplateFileDraft ([reldir|src|] </> path) wasp)
|
||||
[ [relfile|index.js|]
|
||||
, [relfile|index.css|]
|
||||
, [relfile|router.js|]
|
||||
, [relfile|serviceWorker.js|]
|
||||
, [reldir|store|] </> [relfile|index.js|]
|
||||
, [reldir|store|] </> [reldir|middleware|] </> [relfile|logger.js|]
|
||||
]
|
||||
++ PageGenerator.generatePages wasp
|
||||
++ EntityGenerator.generateEntities wasp
|
||||
++ [generateReducersJs wasp]
|
||||
|
||||
generateReducersJs :: Wasp -> FileDraft
|
||||
generateReducersJs wasp = createTemplateFileDraft dstPath srcPath (Just templateData)
|
||||
where
|
||||
srcPath = [reldir|src|] </> [relfile|reducers.js|]
|
||||
dstPath = Common.srcDirPath </> [relfile|reducers.js|]
|
||||
templateData = object
|
||||
[ "wasp" .= wasp
|
||||
, "entities" .= map toEntityData (getEntities wasp)
|
||||
]
|
||||
toEntityData entity = object
|
||||
[ "entity" .= entity
|
||||
, "entityLowerName" .= (Util.toLowerFirst $ entityName entity)
|
||||
, "entityStatePath" .= ("./" ++ (Path.toFilePath $ EntityGenerator.entityStatePathInSrc entity))
|
||||
]
|
||||
|
||||
-- * Helpers
|
||||
|
||||
-- | Creates template file draft that uses given path as both src and dst path
|
||||
-- and wasp as template data.
|
||||
simpleTemplateFileDraft :: Path.RelFile -> Wasp -> FileDraft
|
||||
simpleTemplateFileDraft path wasp = createTemplateFileDraft path path (Just $ toJSON wasp)
|
127
waspc/src/Generator/PageGenerator.hs
Normal file
127
waspc/src/Generator/PageGenerator.hs
Normal file
@ -0,0 +1,127 @@
|
||||
module Generator.PageGenerator
|
||||
( generatePages
|
||||
|
||||
-- EXPORTED ONLY FOR TESTS:
|
||||
, generatePage
|
||||
, generatePageComponent
|
||||
, generatePageStyle
|
||||
) where
|
||||
|
||||
import Data.Maybe (fromJust)
|
||||
import Data.Aeson ((.=), object)
|
||||
import qualified Data.Aeson as Aeson
|
||||
import qualified System.FilePath as FP
|
||||
import Path ((</>), relfile, reldir)
|
||||
import qualified Path
|
||||
import qualified Path.Aliases as Path
|
||||
import qualified Path.Extra as Path
|
||||
|
||||
import qualified Util
|
||||
import Wasp (Wasp)
|
||||
import qualified Wasp
|
||||
import qualified Wasp.Page as WP
|
||||
import qualified Wasp.Style as WStyle
|
||||
import qualified Wasp.JsImport as WJsImport
|
||||
import qualified Wasp.Entity as WEntity
|
||||
import Generator.FileDraft
|
||||
import qualified Generator.Entity as EntityGenerator
|
||||
import qualified Generator.Entity.EntityForm as GEF
|
||||
import qualified Generator.Entity.EntityList as GEL
|
||||
import Generator.ExternalCode.Common (externalCodeDirPathInSrc)
|
||||
import qualified Generator.Common as Common
|
||||
|
||||
|
||||
generatePages :: Wasp -> [FileDraft]
|
||||
generatePages wasp = concatMap (generatePage wasp) (Wasp.getPages wasp)
|
||||
|
||||
generatePage :: Wasp -> WP.Page -> [FileDraft]
|
||||
generatePage wasp page =
|
||||
[ generatePageComponent wasp page
|
||||
]
|
||||
++ maybe [] fst (generatePageStyle wasp page)
|
||||
|
||||
generatePageComponent :: Wasp -> WP.Page -> FileDraft
|
||||
generatePageComponent wasp page = createTemplateFileDraft dstPath srcPath (Just templateData)
|
||||
where
|
||||
srcPath = [reldir|src|] </> [relfile|_Page.js|]
|
||||
dstPath = Common.srcDirPath </> pageDirPathInSrc </> (fromJust $ Path.parseRelFile $ (WP.pageName page) ++ ".js")
|
||||
templateData = object $
|
||||
[ "wasp" .= wasp
|
||||
, "page" .= page
|
||||
, "entities" .= map toEntityData (Wasp.getEntities wasp)
|
||||
, "jsImports" .= map toJsImportData (Wasp.getJsImports wasp)
|
||||
]
|
||||
++ maybe []
|
||||
(\(_, path) -> ["pageStylePath" .= (buildImportPathFromPathInSrc path)])
|
||||
(generatePageStyle wasp page)
|
||||
|
||||
toEntityData entity = object
|
||||
[ "entity" .= entity
|
||||
, "entityLowerName" .= (Util.toLowerFirst $ WEntity.entityName entity)
|
||||
, "entityUpperName" .= (Util.toUpperFirst $ WEntity.entityName entity)
|
||||
, "entityStatePath" .=
|
||||
(buildImportPathFromPathInSrc $ EntityGenerator.entityStatePathInSrc entity)
|
||||
, "entityActionsPath" .=
|
||||
(buildImportPathFromPathInSrc $ EntityGenerator.entityActionsPathInSrc entity)
|
||||
, "entityClassPath" .=
|
||||
(buildImportPathFromPathInSrc $ EntityGenerator.entityClassPathInSrc entity)
|
||||
, "entityCreateForms" .= map toEntityFormData entityForms
|
||||
, "entityLists" .= map toEntityListData entityLists
|
||||
]
|
||||
where
|
||||
-- Entity forms
|
||||
entityForms = Wasp.getEntityFormsForEntity wasp entity
|
||||
generateEntityFormPath entityForm =
|
||||
buildImportPathFromPathInSrc $ GEF.entityCreateFormPathInSrc entity entityForm
|
||||
|
||||
toEntityFormData entityForm = object
|
||||
[ "entityForm" .= entityForm
|
||||
, "path" .= generateEntityFormPath entityForm
|
||||
]
|
||||
|
||||
-- Entity list
|
||||
entityLists = Wasp.getEntityListsForEntity wasp entity
|
||||
generateEntityListPath entityList =
|
||||
buildImportPathFromPathInSrc $ GEL.entityListPathInSrc entity entityList
|
||||
|
||||
toEntityListData entityList = object
|
||||
[ "entityList" .= entityList
|
||||
, "path" .= generateEntityListPath entityList
|
||||
]
|
||||
|
||||
toJsImportData :: WJsImport.JsImport -> Aeson.Value
|
||||
toJsImportData jsImport = object
|
||||
[ "what" .= WJsImport.jsImportWhat jsImport
|
||||
-- NOTE: Here we assume that "from" is relative to external code dir path.
|
||||
-- If this part will be reused, consider externalizing this assumption, so we don't have it on multiple places.
|
||||
, "from" .= (buildImportPathFromPathInSrc $
|
||||
externalCodeDirPathInSrc </> (WJsImport.jsImportFrom jsImport))
|
||||
]
|
||||
|
||||
pageDirPathInSrc :: Path.RelDir
|
||||
pageDirPathInSrc = [reldir|.|]
|
||||
|
||||
relPathFromPageToSrc :: FilePath
|
||||
relPathFromPageToSrc = Path.reversePath pageDirPathInSrc
|
||||
|
||||
-- | Takes path relative to the src path of generated project and turns it into relative path that can be
|
||||
-- used as "from" part of the import in the Page source file.
|
||||
-- NOTE: Here we return FilePath instead of Path because we need stuff like "./" or "../" in the path,
|
||||
-- which Path would normalize away.
|
||||
buildImportPathFromPathInSrc :: Path.Path Path.Rel a -> FilePath
|
||||
buildImportPathFromPathInSrc pathInSrc = relPathFromPageToSrc FP.</> (Path.toFilePath pathInSrc)
|
||||
|
||||
-- Returns file draft(s) that need to be created (if any) +
|
||||
-- file path via which to import the style (relative to generated src dir).
|
||||
generatePageStyle :: Wasp -> WP.Page -> Maybe ([FileDraft], Path.RelFile)
|
||||
generatePageStyle _ page = makeDraftsAndPath <$> WP.pageStyle page
|
||||
where
|
||||
makeDraftsAndPath :: WStyle.Style -> ([FileDraft], Path.RelFile)
|
||||
makeDraftsAndPath (WStyle.CssCode code) =
|
||||
let stylePathInSrcDir = fromJust $ Path.parseRelFile $ (WP.pageName page) ++ ".css"
|
||||
draftDstPath = Common.srcDirPath </> stylePathInSrcDir
|
||||
in ( [createTextFileDraft draftDstPath code]
|
||||
, fromJust $ Path.parseRelFile $ (WP.pageName page) ++ ".css"
|
||||
)
|
||||
makeDraftsAndPath (WStyle.ExtCodeCssFile pathInExtCodeDir) =
|
||||
([], externalCodeDirPathInSrc </> pathInExtCodeDir)
|
86
waspc/src/Generator/Templates.hs
Normal file
86
waspc/src/Generator/Templates.hs
Normal file
@ -0,0 +1,86 @@
|
||||
module Generator.Templates
|
||||
( getTemplatesDirAbsPath
|
||||
, getTemplateFileAbsPath
|
||||
, compileAndRenderTemplate
|
||||
) where
|
||||
|
||||
import qualified Text.Mustache as Mustache
|
||||
import Text.Mustache.Render (SubstitutionError(..))
|
||||
import qualified Data.Aeson as Aeson
|
||||
import Data.Text (Text)
|
||||
import Text.Printf (printf)
|
||||
import Path ((</>), reldir)
|
||||
import qualified Path
|
||||
import qualified Path.Aliases as Path
|
||||
|
||||
import qualified Paths_waspc
|
||||
|
||||
-- TODO: Write tests for this file! But first we need to decouple logic from IO
|
||||
-- so that we can mock it.
|
||||
|
||||
-- | Returns absolute path of templates root directory.
|
||||
-- NOTE(Martin): Here I set it to react-app, which might be one lvl too deep
|
||||
-- and will require some changes in the future, but did not want to
|
||||
-- overengineer for now.
|
||||
getTemplatesDirAbsPath :: IO Path.AbsDir
|
||||
getTemplatesDirAbsPath = do
|
||||
absDataDirPath <- Paths_waspc.getDataDir >>= Path.parseAbsDir
|
||||
return $ absDataDirPath </> templatesDirPathInDataDir
|
||||
|
||||
-- | Takes template file path relative to templates root directory and returns
|
||||
-- its absolute path.
|
||||
getTemplateFileAbsPath :: Path.RelFile -> IO Path.AbsFile
|
||||
getTemplateFileAbsPath tmplFilePathInTemplatesDir =
|
||||
Paths_waspc.getDataFileName (Path.toFilePath tmplFilePathInDataDir) >>= Path.parseAbsFile
|
||||
where
|
||||
tmplFilePathInDataDir = templatesDirPathInDataDir </> tmplFilePathInTemplatesDir
|
||||
|
||||
-- | NOTE(Martin): Here I set it to react-app, which might be one lvl too deep
|
||||
-- and will require some changes in the future, but did not want to
|
||||
-- overengineer for now.
|
||||
templatesDirPathInDataDir :: Path.RelDir
|
||||
templatesDirPathInDataDir = reactAppTemplatesDirPathInDataDir
|
||||
|
||||
reactAppTemplatesDirPathInDataDir :: Path.RelDir
|
||||
reactAppTemplatesDirPathInDataDir = [reldir|Generator|] </> [reldir|templates|] </> [reldir|react-app|]
|
||||
|
||||
compileAndRenderTemplate
|
||||
:: Path.RelFile -- ^ Path to the template file, relative to templates root dir.
|
||||
-> Aeson.Value -- ^ JSON to be provided as template data.
|
||||
-> IO Text
|
||||
compileAndRenderTemplate relTmplPath tmplData = do
|
||||
mustacheTemplate <- compileMustacheTemplate relTmplPath
|
||||
renderMustacheTemplate mustacheTemplate tmplData
|
||||
|
||||
compileMustacheTemplate
|
||||
:: Path.RelFile -- ^ Path to the template file, relative to templates root dir.
|
||||
-> IO Mustache.Template
|
||||
compileMustacheTemplate relTmplPath = do
|
||||
templatesDirAbsPath <- getTemplatesDirAbsPath
|
||||
absTmplPath <- getTemplateFileAbsPath relTmplPath
|
||||
eitherTemplate <- Mustache.automaticCompile [Path.toFilePath templatesDirAbsPath] (Path.toFilePath absTmplPath)
|
||||
return $ either raiseCompileError id eitherTemplate
|
||||
where
|
||||
raiseCompileError err = error $ -- TODO: Handle these errors better?
|
||||
printf "Compilation of template %s failed. %s" (show relTmplPath) (show err)
|
||||
|
||||
areAllErrorsSectionDataNotFound :: [SubstitutionError] -> Bool
|
||||
areAllErrorsSectionDataNotFound subsErrors = all isSectionDataNotFoundError subsErrors
|
||||
where
|
||||
isSectionDataNotFoundError e = case e of
|
||||
SectionTargetNotFound _ -> True
|
||||
_ -> False
|
||||
|
||||
renderMustacheTemplate :: Mustache.Template -> Aeson.Value -> IO Text
|
||||
renderMustacheTemplate mustacheTemplate templateData = do
|
||||
let mustacheTemplateData = Mustache.toMustache templateData
|
||||
let (errors, fileText) =
|
||||
Mustache.checkedSubstituteValue mustacheTemplate mustacheTemplateData
|
||||
|
||||
-- NOTE(matija): Mustache reports errors when object does
|
||||
-- not have a property specified in the template, which we use to implement
|
||||
-- conditionals. This is why we ignore these errors.
|
||||
if (null errors) || (areAllErrorsSectionDataNotFound errors)
|
||||
then (return fileText)
|
||||
else (error $ "Unexpected errors occured while rendering template: "
|
||||
++ (show errors))
|
117
waspc/src/Lexer.hs
Normal file
117
waspc/src/Lexer.hs
Normal file
@ -0,0 +1,117 @@
|
||||
module Lexer where
|
||||
|
||||
import Text.Parsec (letter, alphaNum, (<|>), char, between)
|
||||
import Text.Parsec.String (Parser)
|
||||
import Text.Parsec.Language (emptyDef)
|
||||
import qualified Text.Parsec.Token as Token
|
||||
|
||||
reservedNameImport :: String
|
||||
reservedNameImport = "import"
|
||||
|
||||
reservedNameFrom :: String
|
||||
reservedNameFrom = "from"
|
||||
|
||||
-- * Wasp element types
|
||||
|
||||
reservedNameApp :: String
|
||||
reservedNameApp = "app"
|
||||
|
||||
reservedNamePage :: String
|
||||
reservedNamePage = "page"
|
||||
|
||||
reservedNameEntity :: String
|
||||
reservedNameEntity = "entity"
|
||||
|
||||
reservedNameEntityForm :: String
|
||||
reservedNameEntityForm = "entity-form"
|
||||
|
||||
reservedNameEntityList :: String
|
||||
reservedNameEntityList = "entity-list"
|
||||
|
||||
-- * Data types.
|
||||
|
||||
reservedNameString :: String
|
||||
reservedNameString = "string"
|
||||
|
||||
reservedNameBoolean :: String
|
||||
reservedNameBoolean = "boolean"
|
||||
|
||||
reservedNameBooleanTrue :: String
|
||||
reservedNameBooleanTrue = "true"
|
||||
|
||||
reservedNameBooleanFalse :: String
|
||||
reservedNameBooleanFalse = "false"
|
||||
|
||||
reservedNames :: [String]
|
||||
reservedNames =
|
||||
[ reservedNameImport
|
||||
, reservedNameFrom
|
||||
-- * Wasp element types
|
||||
, reservedNameApp
|
||||
, reservedNamePage
|
||||
, reservedNameEntity
|
||||
, reservedNameEntityForm
|
||||
-- * Data types
|
||||
, reservedNameString
|
||||
, reservedNameBoolean
|
||||
, reservedNameBooleanTrue
|
||||
, reservedNameBooleanFalse
|
||||
]
|
||||
|
||||
waspLanguageDef :: Token.LanguageDef ()
|
||||
waspLanguageDef = emptyDef
|
||||
{ Token.commentLine = "//"
|
||||
, Token.reservedNames = reservedNames
|
||||
, Token.caseSensitive = True
|
||||
-- Identifier
|
||||
, Token.identStart = letter
|
||||
, Token.identLetter = alphaNum <|> char '_'
|
||||
}
|
||||
|
||||
waspLexer :: Token.TokenParser ()
|
||||
waspLexer = Token.makeTokenParser waspLanguageDef
|
||||
|
||||
reserved :: String -> Parser ()
|
||||
reserved = Token.reserved waspLexer
|
||||
|
||||
identifier :: Parser String
|
||||
identifier = Token.identifier waspLexer
|
||||
|
||||
symbol :: String -> Parser String
|
||||
symbol = Token.symbol waspLexer
|
||||
|
||||
colon :: Parser String
|
||||
colon = Token.colon waspLexer
|
||||
|
||||
braces :: Parser a -> Parser a
|
||||
braces = Token.braces waspLexer
|
||||
|
||||
-- Parses content between '<' and '>'.
|
||||
angles :: Parser a -> Parser a
|
||||
angles = Token.angles waspLexer
|
||||
|
||||
commaSep :: Parser a -> Parser [a]
|
||||
commaSep = Token.commaSep waspLexer
|
||||
|
||||
commaSep1 :: Parser a -> Parser [a]
|
||||
commaSep1 = Token.commaSep1 waspLexer
|
||||
|
||||
whiteSpace :: Parser ()
|
||||
whiteSpace = Token.whiteSpace waspLexer
|
||||
|
||||
singleQuotes :: Parser a -> Parser a
|
||||
singleQuotes p = between (symbol "'") (symbol "'") p
|
||||
|
||||
stringLiteral :: Parser String
|
||||
stringLiteral = Token.stringLiteral waspLexer
|
||||
|
||||
-- * Parsing boolean values
|
||||
|
||||
bool :: Parser Bool
|
||||
bool = boolTrue <|> boolFalse
|
||||
|
||||
boolTrue :: Parser Bool
|
||||
boolTrue = reserved reservedNameBooleanTrue *> return True
|
||||
|
||||
boolFalse :: Parser Bool
|
||||
boolFalse = reserved reservedNameBooleanFalse *> return False
|
27
waspc/src/Lib.hs
Normal file
27
waspc/src/Lib.hs
Normal file
@ -0,0 +1,27 @@
|
||||
module Lib
|
||||
( compile
|
||||
) where
|
||||
|
||||
import CompileOptions (CompileOptions)
|
||||
import qualified CompileOptions
|
||||
import qualified ExternalCode
|
||||
import qualified Parser
|
||||
import qualified Generator
|
||||
import Wasp (setExternalCodeFiles)
|
||||
import qualified Path
|
||||
import qualified Path.Aliases as Path
|
||||
|
||||
|
||||
type CompileError = String
|
||||
|
||||
compile :: Path.AbsFile -> Path.AbsDir -> CompileOptions -> IO (Either CompileError ())
|
||||
compile waspFile outDir options = do
|
||||
waspStr <- readFile (Path.toFilePath waspFile)
|
||||
|
||||
case Parser.parseWasp waspStr of
|
||||
Left err -> return $ Left (show err)
|
||||
Right wasp -> do
|
||||
externalCodeFiles <- ExternalCode.readFiles (CompileOptions.externalCodeDirPath options)
|
||||
generateCode $ wasp `setExternalCodeFiles` externalCodeFiles
|
||||
where
|
||||
generateCode wasp = Generator.writeWebAppCode wasp outDir options >> return (Right ())
|
66
waspc/src/Parser.hs
Normal file
66
waspc/src/Parser.hs
Normal file
@ -0,0 +1,66 @@
|
||||
module Parser
|
||||
( parseWasp
|
||||
) where
|
||||
|
||||
import Text.Parsec (ParseError, (<|>), many1, eof, many)
|
||||
import Text.Parsec.String (Parser)
|
||||
|
||||
import qualified Wasp
|
||||
|
||||
import Lexer
|
||||
|
||||
import Parser.App (app)
|
||||
import Parser.Page (page)
|
||||
import Parser.Entity (entity)
|
||||
import Parser.Entity.EntityForm (entityForm)
|
||||
import Parser.Entity.EntityList (entityList)
|
||||
import Parser.JsImport (jsImport)
|
||||
import Parser.Common (runWaspParser)
|
||||
|
||||
|
||||
waspElement :: Parser Wasp.WaspElement
|
||||
waspElement
|
||||
= waspElementApp
|
||||
<|> waspElementPage
|
||||
<|> waspElementEntity
|
||||
<|> waspElementEntityForm
|
||||
<|> waspElementEntityList
|
||||
|
||||
waspElementApp :: Parser Wasp.WaspElement
|
||||
waspElementApp = Wasp.WaspElementApp <$> app
|
||||
|
||||
waspElementPage :: Parser Wasp.WaspElement
|
||||
waspElementPage = Wasp.WaspElementPage <$> page
|
||||
|
||||
waspElementEntity :: Parser Wasp.WaspElement
|
||||
waspElementEntity = Wasp.WaspElementEntity <$> entity
|
||||
|
||||
waspElementEntityForm :: Parser Wasp.WaspElement
|
||||
waspElementEntityForm = Wasp.WaspElementEntityForm <$> entityForm
|
||||
|
||||
waspElementEntityList :: Parser Wasp.WaspElement
|
||||
waspElementEntityList = Wasp.WaspElementEntityList <$> entityList
|
||||
|
||||
-- | Top level parser, produces Wasp.
|
||||
waspParser :: Parser Wasp.Wasp
|
||||
waspParser = do
|
||||
-- NOTE(matija): this is the only place we need to use whiteSpace, to skip empty lines
|
||||
-- and comments in the beginning of file. All other used parsers are lexeme parsers
|
||||
-- so they do it themselves.
|
||||
whiteSpace
|
||||
|
||||
jsImports <- many jsImport
|
||||
|
||||
waspElems <- many1 waspElement
|
||||
|
||||
eof
|
||||
|
||||
-- TODO(matija): after we parsed everything, we should do semantic analysis
|
||||
-- e.g. check there is only 1 title - if not, throw a meaningful error.
|
||||
-- Also, check there is at least one Page defined.
|
||||
|
||||
return $ (Wasp.fromWaspElems waspElems) `Wasp.setJsImports` jsImports
|
||||
|
||||
-- | Top level parser executor.
|
||||
parseWasp :: String -> Either ParseError Wasp.Wasp
|
||||
parseWasp input = runWaspParser waspParser input
|
42
waspc/src/Parser/App.hs
Normal file
42
waspc/src/Parser/App.hs
Normal file
@ -0,0 +1,42 @@
|
||||
module Parser.App
|
||||
( app
|
||||
) where
|
||||
|
||||
import Text.Parsec
|
||||
import Text.Parsec.String (Parser)
|
||||
|
||||
import Lexer
|
||||
import qualified Wasp.App as App
|
||||
import Parser.Common
|
||||
|
||||
-- | A type that describes supported app properties.
|
||||
data AppProperty
|
||||
= Title !String
|
||||
| Favicon !String
|
||||
deriving (Show, Eq)
|
||||
|
||||
-- | Parses supported app properties, expects format "key1: value1, key2: value2, ..."
|
||||
appProperties :: Parser [AppProperty]
|
||||
appProperties = commaSep1 $ appPropertyTitle <|> appPropertyFavicon
|
||||
|
||||
appPropertyTitle :: Parser AppProperty
|
||||
appPropertyTitle = Title <$> waspPropertyStringLiteral "title"
|
||||
|
||||
appPropertyFavicon :: Parser AppProperty
|
||||
-- TODO(matija): 'fav.png' currently does not work because of '.'. Support it.
|
||||
appPropertyFavicon = Favicon <$> waspPropertyStringLiteral "favicon"
|
||||
|
||||
-- TODO(matija): unsafe, what if empty list?
|
||||
getAppTitle :: [AppProperty] -> String
|
||||
getAppTitle ps = head $ [t | Title t <- ps]
|
||||
|
||||
-- | Top level parser, parses App.
|
||||
app :: Parser App.App
|
||||
app = do
|
||||
(appName, appProps) <- waspElementNameAndClosure reservedNameApp appProperties
|
||||
|
||||
return App.App
|
||||
{ App.appName = appName
|
||||
, App.appTitle = getAppTitle appProps
|
||||
-- TODO(matija): add favicon.
|
||||
}
|
137
waspc/src/Parser/Common.hs
Normal file
137
waspc/src/Parser/Common.hs
Normal file
@ -0,0 +1,137 @@
|
||||
{-
|
||||
Common functions used among Wasp parsers.
|
||||
-}
|
||||
|
||||
module Parser.Common where
|
||||
|
||||
import Text.Parsec (ParseError, parse, anyChar, manyTill, try, unexpected)
|
||||
import Text.Parsec.String (Parser)
|
||||
import qualified Data.Text as T
|
||||
import qualified Path
|
||||
import qualified Path.Aliases as Path
|
||||
|
||||
import qualified Lexer as L
|
||||
|
||||
|
||||
-- | Runs given wasp parser on a specified input.
|
||||
runWaspParser :: Parser a -> String -> Either ParseError a
|
||||
runWaspParser waspParser input = parse waspParser sourceName input
|
||||
where
|
||||
-- NOTE(matija): this is used by Parsec only when reporting errors, but we currently
|
||||
-- don't provide source name (e.g. .wasp file name) to this method so leaving it empty
|
||||
-- for now.
|
||||
sourceName = ""
|
||||
|
||||
-- TODO(matija): rename to just "waspElement"?
|
||||
-- | Parses declaration of a wasp element (e.g. App or Page) and the closure content.
|
||||
waspElementNameAndClosure
|
||||
:: String -- ^ Type of the wasp element (e.g. "app" or "page").
|
||||
-> Parser a -- ^ Parser to be used for parsing closure content of the wasp element.
|
||||
-> Parser (String, a) -- ^ Name of the element and parsed closure content.
|
||||
waspElementNameAndClosure elementType closure =
|
||||
-- NOTE(matija): It is important to have `try` here because we don't want to consume the
|
||||
-- content intended for other parsers.
|
||||
-- E.g. if we tried to parse "entity-form" this parser would have been tried first for
|
||||
-- "entity" and would consume "entity", so entity-form parser would also fail.
|
||||
-- This way when entity parser fails, it will backtrack and allow
|
||||
-- entity-form parser to succeed.
|
||||
--
|
||||
-- TODO(matija): should I push this try higher, to the specific case of entity parser
|
||||
-- which is causing the trouble?
|
||||
-- This way try will be executed in more cases where it is not neccessary, this
|
||||
-- might not be the best for the performance and the clarity of error messages.
|
||||
-- On the other hand, it is safer?
|
||||
try $ do
|
||||
-- TODO(matija): should we somehow check if this is a reserved name?
|
||||
L.reserved elementType
|
||||
elementName <- L.identifier
|
||||
closureContent <- waspClosure closure
|
||||
|
||||
return (elementName, closureContent)
|
||||
|
||||
-- | Parses declaration of a wasp element linked to an entity.
|
||||
-- E.g. "entity-form<Task> {...}" or "entity-list<Task> {...}"
|
||||
waspElementLinkedToEntity
|
||||
:: String -- ^ Type of the linked wasp element (e.g. "entity-form").
|
||||
-> Parser a -- ^ Parser to be used for parsing closure content of the wasp element.
|
||||
-> Parser (String, String, a) -- ^ Name of the linked entity, element name and closure content.
|
||||
waspElementLinkedToEntity elementType closure = do
|
||||
L.reserved elementType
|
||||
linkedEntityName <- L.angles L.identifier
|
||||
elementName <- L.identifier
|
||||
closureContent <- waspClosure closure
|
||||
|
||||
return (linkedEntityName, elementName, closureContent)
|
||||
|
||||
-- | Parses wasp property along with the key, "key: value".
|
||||
waspProperty :: String -> Parser a -> Parser a
|
||||
waspProperty key value = L.symbol key <* L.colon *> value
|
||||
|
||||
-- | Parses wasp property which has a string literal for a value.
|
||||
-- e.g.: title: "my first app"
|
||||
waspPropertyStringLiteral :: String -> Parser String
|
||||
waspPropertyStringLiteral key = waspProperty key L.stringLiteral
|
||||
|
||||
-- | Parses wasp property which has a bool for a value. E.g.: "onEnter: true".
|
||||
waspPropertyBool :: String -> Parser Bool
|
||||
waspPropertyBool key = waspProperty key L.bool
|
||||
|
||||
-- | Parses wasp closure, which is {...}. Returns parsed content within the closure.
|
||||
waspClosure :: Parser a -> Parser a
|
||||
waspClosure = L.braces
|
||||
|
||||
-- | Parses wasp property closure where property is an identifier whose value we also
|
||||
-- need to retrieve.
|
||||
-- E.g. within an entity-form {} we can set properties for a specific field with a closure of
|
||||
-- form "FIELD_NAME: {...}" -> FIELD_NAME is then an identifier we need.
|
||||
waspIdentifierClosure :: Parser a -> Parser (String, a)
|
||||
waspIdentifierClosure closureContent = do
|
||||
identifier <- L.identifier <* L.colon
|
||||
content <- waspClosure closureContent
|
||||
|
||||
return (identifier, content)
|
||||
|
||||
-- | Parses wasp property which has a closure for a value. Returns parsed content within the
|
||||
-- closure.
|
||||
waspPropertyClosure :: String -> Parser a -> Parser a
|
||||
waspPropertyClosure key closureContent = waspProperty key (waspClosure closureContent)
|
||||
|
||||
-- | Parses wasp property which has a jsx closure for a value. Returns the content
|
||||
-- within the closure.
|
||||
waspPropertyJsxClosure :: String -> Parser String
|
||||
waspPropertyJsxClosure key = waspProperty key waspJsxClosure
|
||||
|
||||
-- | Parses wasp property which has a css closure for a value. Returns the content
|
||||
-- within the closure.
|
||||
waspPropertyCssClosure :: String -> Parser String
|
||||
waspPropertyCssClosure key = waspProperty key waspCssClosure
|
||||
|
||||
-- | Parses wasp jsx closure, which is {=jsx...jsx=}. Returns content within the closure.
|
||||
waspJsxClosure :: Parser String
|
||||
waspJsxClosure = waspNamedClosure "jsx"
|
||||
|
||||
-- | Parses wasp css closure, which is {=css...css=}. Returns content within the closure.
|
||||
waspCssClosure :: Parser String
|
||||
waspCssClosure = waspNamedClosure "css"
|
||||
|
||||
-- TODO(martin): write tests and comments.
|
||||
-- | Parses wasp css closure, which is {=name...name=}. Returns content within the closure.
|
||||
waspNamedClosure :: String -> Parser String
|
||||
waspNamedClosure name = do
|
||||
_ <- closureStart
|
||||
strip <$> (manyTill anyChar (try closureEnd))
|
||||
where
|
||||
closureStart = L.symbol ("{=" ++ name)
|
||||
closureEnd = L.symbol (name ++ "=}")
|
||||
|
||||
-- | Removes leading and trailing spaces from a string.
|
||||
strip :: String -> String
|
||||
strip = T.unpack . T.strip . T.pack
|
||||
|
||||
-- | Parses relative file path, e.g. "my/file.txt".
|
||||
relFilePathString :: Parser Path.RelFile
|
||||
relFilePathString = do
|
||||
path <- L.stringLiteral
|
||||
maybe (unexpected $ "string \"" ++ path ++ "\": Expected relative file path.")
|
||||
return
|
||||
(Path.parseRelFile path)
|
61
waspc/src/Parser/Entity.hs
Normal file
61
waspc/src/Parser/Entity.hs
Normal file
@ -0,0 +1,61 @@
|
||||
module Parser.Entity
|
||||
( entity
|
||||
|
||||
-- For testing purposes.
|
||||
, entityFieldType
|
||||
, entityField
|
||||
, entityFields
|
||||
) where
|
||||
|
||||
import Text.Parsec ((<|>))
|
||||
import Text.Parsec.String (Parser)
|
||||
|
||||
import Lexer
|
||||
( identifier
|
||||
, colon
|
||||
, commaSep1
|
||||
, reserved
|
||||
, reservedNameEntity
|
||||
, reservedNameString
|
||||
, reservedNameBoolean
|
||||
)
|
||||
import qualified Wasp.Entity as Entity
|
||||
import qualified Parser.Common as P
|
||||
|
||||
-- | Top level parser, parses Entity.
|
||||
entity :: Parser Entity.Entity
|
||||
entity = do
|
||||
(name, fields) <- P.waspElementNameAndClosure reservedNameEntity entityFields
|
||||
|
||||
return Entity.Entity
|
||||
{ Entity.entityName = name
|
||||
, Entity.entityFields = fields
|
||||
}
|
||||
|
||||
-- Parses entity fields.
|
||||
entityFields :: Parser [Entity.EntityField]
|
||||
entityFields = commaSep1 entityField
|
||||
|
||||
-- | Parses a single entity field, e.g. "title :: string".
|
||||
entityField :: Parser Entity.EntityField
|
||||
entityField = do
|
||||
fieldName <- identifier
|
||||
_ <- typeAssignmentOp
|
||||
fieldType <- entityFieldType
|
||||
|
||||
return $ Entity.EntityField fieldName fieldType
|
||||
where
|
||||
-- TODO(matija): maybe specify it under reservedOps in Lexer?
|
||||
typeAssignmentOp = colon *> colon
|
||||
|
||||
-- | Parses one of the supported types of the entity field e.g. "string" or "boolean".
|
||||
entityFieldType :: Parser Entity.EntityFieldType
|
||||
entityFieldType = entityFieldTypeString <|> entityFieldTypeBoolean
|
||||
|
||||
-- | Parses string type of entity field.
|
||||
entityFieldTypeString :: Parser Entity.EntityFieldType
|
||||
entityFieldTypeString = reserved reservedNameString *> pure Entity.EftString
|
||||
|
||||
-- | Parses boolean type of entity field.
|
||||
entityFieldTypeBoolean :: Parser Entity.EntityFieldType
|
||||
entityFieldTypeBoolean = reserved reservedNameBoolean *> pure Entity.EftBoolean
|
39
waspc/src/Parser/Entity/Common.hs
Normal file
39
waspc/src/Parser/Entity/Common.hs
Normal file
@ -0,0 +1,39 @@
|
||||
module Parser.Entity.Common
|
||||
( waspPropertyEntityFields
|
||||
) where
|
||||
|
||||
import Text.Parsec.String (Parser)
|
||||
|
||||
import qualified Lexer as L
|
||||
import Parser.Common as P
|
||||
|
||||
-- A function that takes an entity field name (e.g. "description) and a list of parsed field
|
||||
-- options, and then creates a final Wasp AST record from it (fieldConfig).
|
||||
type CreateFieldConfig fieldOption fieldConfig = (String, [fieldOption]) -> fieldConfig
|
||||
|
||||
-- | Parses configuration of fields within a wasp entity component (e.g. entity-form
|
||||
-- or entity-list). Parses the following format:
|
||||
--
|
||||
-- fields: { FIELD_NAME: {...}, FIELD_NAME: {...}, ... }
|
||||
--
|
||||
-- At least one field must be specified.
|
||||
waspPropertyEntityFields
|
||||
:: Parser fo -- ^ Parser of a single field option.
|
||||
-> CreateFieldConfig fo fc -- ^ Function that creates a record with all parsed field options.
|
||||
-> Parser [fc] -- ^ Field configs, a list of record with all the field options.
|
||||
waspPropertyEntityFields fieldOptionP createFieldConfig = P.waspPropertyClosure "fields" $
|
||||
L.commaSep1 $ waspPropertyEntityField fieldOptionP createFieldConfig
|
||||
|
||||
|
||||
-- | Parses configuration of a specific field within a wasp entity component (e.g. entity-form
|
||||
-- or entity-list). Parses the following format:
|
||||
--
|
||||
-- FIELD_NAME: { option1, option2 }
|
||||
--
|
||||
-- At least one option must be present.
|
||||
waspPropertyEntityField
|
||||
:: Parser fo -- ^ Parser of a single field option.
|
||||
-> CreateFieldConfig fo fc -- ^ Function that creates a record with all parsed field options.
|
||||
-> Parser fc -- ^ Field config, a record with all the field options.
|
||||
waspPropertyEntityField fieldOptionP createFieldConfig =
|
||||
(P.waspIdentifierClosure $ L.commaSep1 fieldOptionP) >>= (return . createFieldConfig)
|
166
waspc/src/Parser/Entity/EntityForm.hs
Normal file
166
waspc/src/Parser/Entity/EntityForm.hs
Normal file
@ -0,0 +1,166 @@
|
||||
module Parser.Entity.EntityForm
|
||||
( entityForm
|
||||
|
||||
-- For testing
|
||||
, entityFormOptionSubmit
|
||||
, EntityFormOption (..)
|
||||
|
||||
, submitConfig
|
||||
) where
|
||||
|
||||
import Text.Parsec (choice, (<|>))
|
||||
import Text.Parsec.String (Parser)
|
||||
|
||||
import qualified Wasp.EntityForm as WEF
|
||||
import Wasp.EntityForm (EntityForm)
|
||||
|
||||
import qualified Parser.Common as P
|
||||
import qualified Parser.Entity.Common as PE
|
||||
import qualified Util as U
|
||||
import qualified Lexer as L
|
||||
|
||||
|
||||
-- * EntityForm
|
||||
|
||||
-- | Parses entity form, e.g. "entity-form<Task> NewTaskForm {...}"
|
||||
entityForm :: Parser EntityForm
|
||||
entityForm = do
|
||||
(entityName, formName, options) <-
|
||||
P.waspElementLinkedToEntity L.reservedNameEntityForm entityFormOptions
|
||||
|
||||
return WEF.EntityForm
|
||||
{ WEF._name = formName
|
||||
, WEF._entityName = entityName
|
||||
, WEF._submit = maybeGetSubmitConfig options
|
||||
, WEF._fields = getFieldsConfig options
|
||||
}
|
||||
|
||||
data EntityFormOption
|
||||
= EfoSubmit WEF.Submit
|
||||
| EfoFields [WEF.Field]
|
||||
deriving (Show, Eq)
|
||||
|
||||
entityFormOptions :: Parser [EntityFormOption]
|
||||
entityFormOptions = L.commaSep entityFormOption
|
||||
|
||||
entityFormOption :: Parser EntityFormOption
|
||||
entityFormOption = choice
|
||||
[ entityFormOptionSubmit
|
||||
, entityFormOptionFields
|
||||
]
|
||||
|
||||
-- * Submit
|
||||
|
||||
maybeGetSubmitConfig :: [EntityFormOption] -> Maybe WEF.Submit
|
||||
maybeGetSubmitConfig options = U.headSafe [s | EfoSubmit s <- options]
|
||||
|
||||
entityFormOptionSubmit :: Parser EntityFormOption
|
||||
entityFormOptionSubmit = EfoSubmit <$> (P.waspPropertyClosure "submit" submitConfig)
|
||||
|
||||
submitConfig :: Parser WEF.Submit
|
||||
submitConfig = do
|
||||
-- TODO(matija): this pattern of "having at least 1 property in closure" could be further
|
||||
-- extracted to e.g. "waspClosureOptions" - but again sometimes it is ok not to have any props,
|
||||
-- e.g. EntityForm. Maybe then "waspClosureOptions1" and "waspClosureOptions"?
|
||||
options <- L.commaSep1 submitOption
|
||||
|
||||
return WEF.Submit
|
||||
{ WEF._onEnter = maybeGetSoOnEnter options
|
||||
, WEF._submitButton = maybeGetSoSubmitButton options
|
||||
}
|
||||
|
||||
data SubmitOption = SoOnEnter Bool | SoSubmitButton WEF.SubmitButton deriving (Show, Eq)
|
||||
|
||||
submitOption :: Parser SubmitOption
|
||||
submitOption = choice [submitOptionOnEnter, submitOptionSubmitButton]
|
||||
|
||||
-- onEnter
|
||||
submitOptionOnEnter :: Parser SubmitOption
|
||||
submitOptionOnEnter = SoOnEnter <$> P.waspPropertyBool "onEnter"
|
||||
|
||||
maybeGetSoOnEnter :: [SubmitOption] -> Maybe Bool
|
||||
maybeGetSoOnEnter options = U.headSafe [b | SoOnEnter b <- options]
|
||||
|
||||
-- submit button
|
||||
submitOptionSubmitButton :: Parser SubmitOption
|
||||
submitOptionSubmitButton = SoSubmitButton <$> P.waspPropertyClosure "button" submitButtonConfig
|
||||
|
||||
maybeGetSoSubmitButton :: [SubmitOption] -> Maybe WEF.SubmitButton
|
||||
maybeGetSoSubmitButton options = U.headSafe [sb | SoSubmitButton sb <- options]
|
||||
|
||||
submitButtonConfig :: Parser WEF.SubmitButton
|
||||
submitButtonConfig = do
|
||||
options <- L.commaSep1 submitButtonOption
|
||||
|
||||
return WEF.SubmitButton
|
||||
{ WEF._submitButtonShow = maybeGetSboShow options
|
||||
}
|
||||
|
||||
data SubmitButtonOption = SboShow Bool deriving (Show, Eq)
|
||||
|
||||
submitButtonOption :: Parser SubmitButtonOption
|
||||
submitButtonOption = submitButtonOptionShow -- <|> anotherOption <|> ...
|
||||
|
||||
submitButtonOptionShow :: Parser SubmitButtonOption
|
||||
submitButtonOptionShow = SboShow <$> P.waspPropertyBool "show"
|
||||
|
||||
maybeGetSboShow :: [SubmitButtonOption] -> Maybe Bool
|
||||
maybeGetSboShow options = U.headSafe [b | SboShow b <- options]
|
||||
|
||||
-- * Fields
|
||||
|
||||
getFieldsConfig :: [EntityFormOption] -> [WEF.Field]
|
||||
getFieldsConfig options = case [fs | EfoFields fs <- options] of
|
||||
[] -> []
|
||||
ls -> head ls
|
||||
|
||||
entityFormOptionFields :: Parser EntityFormOption
|
||||
entityFormOptionFields = EfoFields <$> PE.waspPropertyEntityFields fieldOption createFieldConfig
|
||||
|
||||
createFieldConfig :: (String, [FieldOption]) -> WEF.Field
|
||||
createFieldConfig (fieldName, options) = WEF.Field
|
||||
{ WEF._fieldName = fieldName
|
||||
, WEF._fieldShow = maybeGetFieldOptionShow options
|
||||
, WEF._fieldDefaultValue = maybeGetFieldOptionDefaultValue options
|
||||
, WEF._fieldPlaceholder = maybeGetFieldOptionPlaceholder options
|
||||
, WEF._fieldLabel = maybeGetFieldOptionLabel options
|
||||
}
|
||||
|
||||
data FieldOption
|
||||
= FieldOptionShow Bool
|
||||
| FieldOptionDefaultValue WEF.DefaultValue
|
||||
| FieldOptionPlaceholder String
|
||||
| FieldOptionLabel (Maybe String)
|
||||
deriving (Show, Eq)
|
||||
|
||||
-- | Parses a single field option, e.g. "show" or "defaultValue".
|
||||
fieldOption :: Parser FieldOption
|
||||
fieldOption = choice
|
||||
[ FieldOptionShow <$> P.waspPropertyBool "show"
|
||||
, FieldOptionDefaultValue <$> defaultValue
|
||||
, FieldOptionPlaceholder <$> P.waspPropertyStringLiteral "placeholder"
|
||||
, FieldOptionLabel <$> fieldOptionLabel
|
||||
]
|
||||
|
||||
defaultValue :: Parser WEF.DefaultValue
|
||||
defaultValue = P.waspProperty "defaultValue" $ choice
|
||||
[ WEF.DefaultValueString <$> L.stringLiteral
|
||||
, WEF.DefaultValueBool <$> L.bool
|
||||
]
|
||||
|
||||
fieldOptionLabel :: Parser (Maybe String)
|
||||
fieldOptionLabel = P.waspProperty "label" labelValue
|
||||
where
|
||||
labelValue = (Just <$> L.stringLiteral) <|> (L.symbol "none" *> pure Nothing)
|
||||
|
||||
maybeGetFieldOptionShow :: [FieldOption] -> Maybe Bool
|
||||
maybeGetFieldOptionShow options = U.headSafe [b | FieldOptionShow b <- options]
|
||||
|
||||
maybeGetFieldOptionDefaultValue :: [FieldOption] -> Maybe WEF.DefaultValue
|
||||
maybeGetFieldOptionDefaultValue options = U.headSafe [dv | FieldOptionDefaultValue dv <- options]
|
||||
|
||||
maybeGetFieldOptionPlaceholder :: [FieldOption] -> Maybe String
|
||||
maybeGetFieldOptionPlaceholder options = U.headSafe [s | FieldOptionPlaceholder s <- options]
|
||||
|
||||
maybeGetFieldOptionLabel :: [FieldOption] -> Maybe (Maybe String)
|
||||
maybeGetFieldOptionLabel options = U.headSafe [ms | FieldOptionLabel ms <- options]
|
83
waspc/src/Parser/Entity/EntityList.hs
Normal file
83
waspc/src/Parser/Entity/EntityList.hs
Normal file
@ -0,0 +1,83 @@
|
||||
module Parser.Entity.EntityList
|
||||
( entityList
|
||||
) where
|
||||
|
||||
import Text.Parsec (choice)
|
||||
import Text.Parsec.String (Parser)
|
||||
|
||||
import qualified Wasp.EntityList as WEL
|
||||
import Wasp.EntityList (EntityList)
|
||||
|
||||
import qualified Wasp.JsCode as WJS
|
||||
|
||||
import qualified Parser.JsCode
|
||||
import qualified Parser.Common as P
|
||||
import qualified Parser.Entity.Common as PE
|
||||
import qualified Util as U
|
||||
import qualified Lexer as L
|
||||
|
||||
-- * EntityList
|
||||
|
||||
-- | Parses entity list, e.g. "entity-list<Task> TaskList {...}"
|
||||
entityList :: Parser EntityList
|
||||
entityList = do
|
||||
(entityName, listName, options) <-
|
||||
P.waspElementLinkedToEntity L.reservedNameEntityList entityListOptions
|
||||
|
||||
return WEL.EntityList
|
||||
{ WEL._name = listName
|
||||
, WEL._entityName = entityName
|
||||
, WEL._showHeader = maybeGetListOptionShowHeader options
|
||||
, WEL._fields = getFieldsConfig options
|
||||
}
|
||||
|
||||
data EntityListOption
|
||||
= EloShowHeader Bool
|
||||
| EloFields [WEL.Field]
|
||||
deriving (Show, Eq)
|
||||
|
||||
entityListOptions :: Parser [EntityListOption]
|
||||
-- TODO(matija): this could be further abstracted as waspClosureOptions option ->
|
||||
-- that way we abstract L.commaSep
|
||||
entityListOptions = L.commaSep entityListOption
|
||||
|
||||
entityListOption :: Parser EntityListOption
|
||||
entityListOption = choice
|
||||
[ EloShowHeader <$> P.waspPropertyBool "showHeader"
|
||||
, entityListOptionFields
|
||||
]
|
||||
|
||||
-- * Show header
|
||||
|
||||
maybeGetListOptionShowHeader :: [EntityListOption] -> Maybe Bool
|
||||
maybeGetListOptionShowHeader options = U.headSafe [b | EloShowHeader b <- options]
|
||||
|
||||
-- * Fields
|
||||
|
||||
getFieldsConfig :: [EntityListOption] -> [WEL.Field]
|
||||
getFieldsConfig options = case [fs | EloFields fs <- options] of
|
||||
[] -> []
|
||||
ls -> head ls
|
||||
|
||||
entityListOptionFields :: Parser EntityListOption
|
||||
entityListOptionFields = EloFields <$> PE.waspPropertyEntityFields fieldOption createFieldConfig
|
||||
|
||||
createFieldConfig :: (String, [FieldOption]) -> WEL.Field
|
||||
createFieldConfig (fieldName, options) = WEL.Field
|
||||
{ WEL._fieldName = fieldName
|
||||
, WEL._fieldRender = maybeGetFieldOptionRender options
|
||||
}
|
||||
|
||||
data FieldOption
|
||||
= FieldOptionRender WJS.JsCode
|
||||
|
||||
fieldOption :: Parser FieldOption
|
||||
fieldOption = choice
|
||||
[ fieldOptionRender
|
||||
]
|
||||
|
||||
fieldOptionRender :: Parser FieldOption
|
||||
fieldOptionRender = FieldOptionRender <$> P.waspProperty "render" Parser.JsCode.jsCode
|
||||
|
||||
maybeGetFieldOptionRender :: [FieldOption] -> Maybe WJS.JsCode
|
||||
maybeGetFieldOptionRender options = U.headSafe [js | FieldOptionRender js <- options]
|
21
waspc/src/Parser/ExternalCode.hs
Normal file
21
waspc/src/Parser/ExternalCode.hs
Normal file
@ -0,0 +1,21 @@
|
||||
module Parser.ExternalCode
|
||||
( extCodeFilePathString
|
||||
) where
|
||||
|
||||
import Text.Parsec (unexpected)
|
||||
import Text.Parsec.String (Parser)
|
||||
import Path (reldir)
|
||||
import qualified Path
|
||||
import qualified Path.Aliases as Path
|
||||
|
||||
import qualified Parser.Common
|
||||
|
||||
-- Parses string literal that is file path to file in external code dir.
|
||||
-- Returns file path relative to the external code dir.
|
||||
-- Example of input: "@ext/some/file.txt". Output would be: "some/file.txt".
|
||||
extCodeFilePathString :: Parser Path.RelFile
|
||||
extCodeFilePathString = do
|
||||
path <- Parser.Common.relFilePathString
|
||||
maybe (unexpected $ "string \"" ++ (show path) ++ "\": External code file path should start with \"@ext/\".")
|
||||
return
|
||||
(Path.stripProperPrefix [reldir|@ext|] path)
|
12
waspc/src/Parser/JsCode.hs
Normal file
12
waspc/src/Parser/JsCode.hs
Normal file
@ -0,0 +1,12 @@
|
||||
module Parser.JsCode
|
||||
( jsCode
|
||||
) where
|
||||
|
||||
import Text.Parsec.String (Parser)
|
||||
import qualified Data.Text as Text
|
||||
|
||||
import qualified Parser.Common as P
|
||||
import qualified Wasp.JsCode as WJS
|
||||
|
||||
jsCode :: Parser WJS.JsCode
|
||||
jsCode = (WJS.JsCode . Text.pack) <$> P.waspNamedClosure "js"
|
25
waspc/src/Parser/JsImport.hs
Normal file
25
waspc/src/Parser/JsImport.hs
Normal file
@ -0,0 +1,25 @@
|
||||
module Parser.JsImport
|
||||
( jsImport
|
||||
) where
|
||||
|
||||
import Text.Parsec (manyTill, anyChar, try)
|
||||
import Text.Parsec.Char (space)
|
||||
import Text.Parsec.String (Parser)
|
||||
|
||||
import qualified Parser.ExternalCode
|
||||
import qualified Lexer as L
|
||||
import qualified Wasp.JsImport
|
||||
|
||||
|
||||
-- import ... from "..."
|
||||
jsImport :: Parser Wasp.JsImport.JsImport
|
||||
jsImport = do
|
||||
L.whiteSpace
|
||||
_ <- L.reserved L.reservedNameImport
|
||||
-- TODO: In the future, we could further tighten up this parser so it strictly follows format of js import statement.
|
||||
-- Right now it is more relaxed -> it allows almost anything between "import" and "from".
|
||||
what <- anyChar `manyTill` (try (space *> L.whiteSpace *> L.reserved L.reservedNameFrom))
|
||||
-- TODO: For now we only support double quotes here, we should also support single quotes.
|
||||
-- We would need to write this from scratch, with single quote escaping enabled.
|
||||
from <- Parser.ExternalCode.extCodeFilePathString
|
||||
return Wasp.JsImport.JsImport { Wasp.JsImport.jsImportWhat = what, Wasp.JsImport.jsImportFrom = from }
|
64
waspc/src/Parser/Page.hs
Normal file
64
waspc/src/Parser/Page.hs
Normal file
@ -0,0 +1,64 @@
|
||||
module Parser.Page
|
||||
( page
|
||||
) where
|
||||
|
||||
import Text.Parsec
|
||||
import Text.Parsec.String (Parser)
|
||||
import Data.Maybe (listToMaybe)
|
||||
|
||||
import Lexer
|
||||
import qualified Wasp.Page as Page
|
||||
import qualified Wasp.Style
|
||||
import Parser.Common
|
||||
import qualified Parser.Style
|
||||
|
||||
data PageProperty
|
||||
= Title !String
|
||||
| Route !String
|
||||
| Content !String
|
||||
| Style !Wasp.Style.Style
|
||||
deriving (Show, Eq)
|
||||
|
||||
-- | Parses Page properties, separated by a comma.
|
||||
pageProperties :: Parser [PageProperty]
|
||||
pageProperties = commaSep1 $
|
||||
pagePropertyTitle
|
||||
<|> pagePropertyRoute
|
||||
<|> pagePropertyContent
|
||||
<|> pagePropertyStyle
|
||||
|
||||
pagePropertyTitle :: Parser PageProperty
|
||||
pagePropertyTitle = Title <$> waspPropertyStringLiteral "title"
|
||||
|
||||
pagePropertyRoute :: Parser PageProperty
|
||||
pagePropertyRoute = Route <$> waspPropertyStringLiteral "route"
|
||||
|
||||
pagePropertyContent :: Parser PageProperty
|
||||
pagePropertyContent = Content <$> waspPropertyJsxClosure "content"
|
||||
|
||||
pagePropertyStyle :: Parser PageProperty
|
||||
pagePropertyStyle = Style <$> waspProperty "style" Parser.Style.style
|
||||
|
||||
-- TODO(matija): unsafe, what if empty list?
|
||||
getPageRoute :: [PageProperty] -> String
|
||||
-- TODO(matija): we are repeating this pattern. How can we extract it? Consider using
|
||||
-- Template Haskell, lens and prism.
|
||||
getPageRoute ps = head $ [r | Route r <- ps]
|
||||
|
||||
getPageContent :: [PageProperty] -> String
|
||||
getPageContent ps = head $ [c | Content c <- ps]
|
||||
|
||||
getPageStyle :: [PageProperty] -> Maybe Wasp.Style.Style
|
||||
getPageStyle ps = listToMaybe [s | Style s <- ps]
|
||||
|
||||
-- | Top level parser, parses Page.
|
||||
page :: Parser Page.Page
|
||||
page = do
|
||||
(pageName, pageProps) <- waspElementNameAndClosure reservedNamePage pageProperties
|
||||
|
||||
return Page.Page
|
||||
{ Page.pageName = pageName
|
||||
, Page.pageRoute = getPageRoute pageProps
|
||||
, Page.pageContent = getPageContent pageProps
|
||||
, Page.pageStyle = getPageStyle pageProps
|
||||
}
|
21
waspc/src/Parser/Style.hs
Normal file
21
waspc/src/Parser/Style.hs
Normal file
@ -0,0 +1,21 @@
|
||||
module Parser.Style
|
||||
( style
|
||||
) where
|
||||
|
||||
import Text.Parsec ((<|>))
|
||||
import Text.Parsec.String (Parser)
|
||||
import qualified Data.Text as Text
|
||||
|
||||
import qualified Parser.Common
|
||||
import qualified Parser.ExternalCode
|
||||
import qualified Wasp.Style
|
||||
|
||||
|
||||
style :: Parser Wasp.Style.Style
|
||||
style = cssFile <|> cssCode
|
||||
|
||||
cssFile :: Parser Wasp.Style.Style
|
||||
cssFile = Wasp.Style.ExtCodeCssFile <$> Parser.ExternalCode.extCodeFilePathString
|
||||
|
||||
cssCode :: Parser Wasp.Style.Style
|
||||
cssCode = (Wasp.Style.CssCode . Text.pack) <$> Parser.Common.waspNamedClosure "css"
|
9
waspc/src/Path/Aliases.hs
Normal file
9
waspc/src/Path/Aliases.hs
Normal file
@ -0,0 +1,9 @@
|
||||
module Path.Aliases where
|
||||
|
||||
import Path (Path, Abs, Rel, Dir, File)
|
||||
|
||||
|
||||
type RelFile = Path Rel File
|
||||
type AbsFile = Path Abs File
|
||||
type RelDir = Path Rel Dir
|
||||
type AbsDir = Path Abs Dir
|
29
waspc/src/Path/Extra.hs
Normal file
29
waspc/src/Path/Extra.hs
Normal file
@ -0,0 +1,29 @@
|
||||
module Path.Extra
|
||||
( (./)
|
||||
, reversePath
|
||||
) where
|
||||
|
||||
import Control.Exception (assert)
|
||||
import System.FilePath as FP
|
||||
import Path
|
||||
|
||||
|
||||
-- NOTE: Here we return FilePath, because Path can't keep the "./" in it,
|
||||
-- since it always normalizes the path. So the only way to have path with "./" in it
|
||||
-- is to have it as a FilePath.
|
||||
(./) :: Path Rel a -> FP.FilePath
|
||||
(./) relPath = "." FP.</> (toFilePath relPath)
|
||||
|
||||
-- | For given path P, returns path P', such that (terminal pseudocode incoming)
|
||||
-- `pwd == (cd P && cd P' && pwd)`, or to put it differently, such that
|
||||
-- `cd P && cd P'` is noop (does nothing).
|
||||
-- It will always be either "." (only if input is ".") or a series of ".."
|
||||
-- (e.g. reversePath [reldir|foo/bar|] == "../..").
|
||||
reversePath :: Path Rel Dir -> FilePath
|
||||
reversePath path
|
||||
| length parts == 0 = "."
|
||||
| otherwise = assert (not (".." `elem` parts)) $
|
||||
FP.joinPath $ map (const "..") parts
|
||||
where
|
||||
parts :: [String]
|
||||
parts = filter (not . (== ".")) $ FP.splitDirectories $ toFilePath path
|
44
waspc/src/Util.hs
Normal file
44
waspc/src/Util.hs
Normal file
@ -0,0 +1,44 @@
|
||||
module Util
|
||||
( camelToKebabCase
|
||||
, onFirst
|
||||
, toLowerFirst
|
||||
, toUpperFirst
|
||||
, headSafe
|
||||
, jsonSet
|
||||
) where
|
||||
|
||||
import Data.Char (isUpper, toLower, toUpper)
|
||||
import qualified Data.Aeson as Aeson
|
||||
import qualified Data.Text as Text
|
||||
import qualified Data.HashMap.Strict as M
|
||||
|
||||
|
||||
camelToKebabCase :: String -> String
|
||||
camelToKebabCase "" = ""
|
||||
camelToKebabCase camel@(camelHead:camelTail) = kebabHead:kebabTail
|
||||
where
|
||||
kebabHead = toLower camelHead
|
||||
kebabTail = concat $ map
|
||||
(\(a, b) -> (if (isCamelHump (a, b)) then ['-'] else []) ++ [toLower b])
|
||||
(zip camel camelTail)
|
||||
isCamelHump (a, b) = (not . isUpper) a && isUpper b
|
||||
|
||||
-- | Applies given function to the first element of the list.
|
||||
-- If list is empty, returns empty list.
|
||||
onFirst :: (a -> a) -> [a] -> [a]
|
||||
onFirst _ [] = []
|
||||
onFirst f (x:xs) = (f x):xs
|
||||
|
||||
toLowerFirst :: String -> String
|
||||
toLowerFirst = onFirst toLower
|
||||
|
||||
toUpperFirst :: String -> String
|
||||
toUpperFirst = onFirst toUpper
|
||||
|
||||
headSafe :: [a] -> Maybe a
|
||||
headSafe [] = Nothing
|
||||
headSafe xs = Just (head xs)
|
||||
|
||||
jsonSet :: Text.Text -> Aeson.Value -> Aeson.Value -> Aeson.Value
|
||||
jsonSet key value (Aeson.Object o) = Aeson.Object $ M.insert key value o
|
||||
jsonSet _ _ _ = error "Input JSON must be an object"
|
9
waspc/src/Util/Fib.hs
Normal file
9
waspc/src/Util/Fib.hs
Normal file
@ -0,0 +1,9 @@
|
||||
module Util.Fib (
|
||||
fibonacci
|
||||
) where
|
||||
|
||||
fibonacci :: Int -> Int
|
||||
fibonacci 0 = 0
|
||||
fibonacci 1 = 1
|
||||
fibonacci n | n > 1 = (fibonacci (n - 1)) + (fibonacci (n - 2))
|
||||
fibonacci _ = undefined
|
57
waspc/src/Util/IO.hs
Normal file
57
waspc/src/Util/IO.hs
Normal file
@ -0,0 +1,57 @@
|
||||
module Util.IO
|
||||
( listDirectoryDeep
|
||||
) where
|
||||
|
||||
import qualified System.Directory as Dir
|
||||
import qualified System.FilePath as FilePath
|
||||
import System.IO.Error (isDoesNotExistError)
|
||||
import Control.Exception (catch, throw)
|
||||
import Control.Monad (filterM, mapM)
|
||||
import qualified Path
|
||||
|
||||
import qualified Path.Aliases as Path
|
||||
|
||||
|
||||
-- TODO: write tests.
|
||||
-- | Lists all files in the directory recursively.
|
||||
-- All paths are relative to the directory we are listing.
|
||||
-- If directory does not exist, returns empty list.
|
||||
--
|
||||
-- Example: Imagine we have directory foo that contains test.txt and bar/test2.txt.
|
||||
-- If we call
|
||||
-- >>> listDirectoryDeep "foo/"
|
||||
-- we should get
|
||||
-- >>> ["test.txt", "bar/text2.txt"]
|
||||
listDirectoryDeep :: Path.AbsDir -> IO [Path.RelFile]
|
||||
listDirectoryDeep absDirPath = do
|
||||
(relFilePaths, relSubDirPaths) <- (listDirectory absDirPath)
|
||||
`catch` \e -> if isDoesNotExistError e then return ([], []) else throw e
|
||||
relSubDirFilesPaths <- mapM (listSubDirDeep . (absDirPath Path.</>)) relSubDirPaths
|
||||
return $ relFilePaths ++ (concat relSubDirFilesPaths)
|
||||
where
|
||||
-- | NOTE: Here, returned paths are relative to the main dir whose sub dir we are listing,
|
||||
-- which is one level above what you might intuitively expect.
|
||||
listSubDirDeep :: Path.AbsDir -> IO [Path.RelFile]
|
||||
listSubDirDeep subDirPath = do
|
||||
files <- listDirectoryDeep subDirPath
|
||||
return $ map ((Path.dirname subDirPath) Path.</>) files
|
||||
|
||||
-- TODO: write tests.
|
||||
-- | Lists files and directories at top lvl of the directory.
|
||||
listDirectory :: Path.AbsDir -> IO ([Path.RelFile], [Path.RelDir])
|
||||
listDirectory absDirPath = do
|
||||
fpRelItemPaths <- Dir.listDirectory fpAbsDirPath
|
||||
relFilePaths <- filterFiles fpAbsDirPath fpRelItemPaths
|
||||
relDirPaths <- filterDirs fpAbsDirPath fpRelItemPaths
|
||||
return (relFilePaths, relDirPaths)
|
||||
where
|
||||
fpAbsDirPath :: FilePath
|
||||
fpAbsDirPath = Path.toFilePath absDirPath
|
||||
|
||||
filterFiles :: FilePath -> [FilePath] -> IO [Path.RelFile]
|
||||
filterFiles absDir relItems = filterM (Dir.doesFileExist . (absDir FilePath.</>)) relItems
|
||||
>>= mapM Path.parseRelFile
|
||||
|
||||
filterDirs :: FilePath -> [FilePath] -> IO [Path.RelDir]
|
||||
filterDirs absDir relItems = filterM (Dir.doesDirectoryExist . (absDir FilePath.</>)) relItems
|
||||
>>= mapM Path.parseRelDir
|
149
waspc/src/Wasp.hs
Normal file
149
waspc/src/Wasp.hs
Normal file
@ -0,0 +1,149 @@
|
||||
module Wasp
|
||||
( Wasp
|
||||
, WaspElement (..)
|
||||
, fromWaspElems
|
||||
|
||||
, module Wasp.JsImport
|
||||
, getJsImports
|
||||
, setJsImports
|
||||
|
||||
, module Wasp.App
|
||||
, fromApp
|
||||
, getApp
|
||||
, setApp
|
||||
|
||||
, module Wasp.Entity
|
||||
, getEntities
|
||||
, addEntity
|
||||
, getEntityByName
|
||||
|
||||
, getEntityFormsForEntity
|
||||
, getEntityListsForEntity
|
||||
|
||||
, module Wasp.Page
|
||||
, getPages
|
||||
, addPage
|
||||
|
||||
, setExternalCodeFiles
|
||||
, getExternalCodeFiles
|
||||
) where
|
||||
|
||||
import Data.Aeson ((.=), object, ToJSON(..))
|
||||
|
||||
import qualified ExternalCode
|
||||
import Wasp.App
|
||||
import Wasp.Entity
|
||||
import qualified Wasp.EntityForm as EF
|
||||
import qualified Wasp.EntityList as EL
|
||||
import Wasp.JsImport
|
||||
import Wasp.Page
|
||||
|
||||
import qualified Util as U
|
||||
|
||||
-- * Wasp
|
||||
|
||||
data Wasp = Wasp
|
||||
{ waspElements :: [WaspElement]
|
||||
, waspJsImports :: [JsImport]
|
||||
, externalCodeFiles :: [ExternalCode.File]
|
||||
} deriving (Show, Eq)
|
||||
|
||||
data WaspElement
|
||||
= WaspElementApp !App
|
||||
| WaspElementPage !Page
|
||||
| WaspElementEntity !Entity
|
||||
| WaspElementEntityForm !EF.EntityForm
|
||||
| WaspElementEntityList !EL.EntityList
|
||||
deriving (Show, Eq)
|
||||
|
||||
fromWaspElems :: [WaspElement] -> Wasp
|
||||
fromWaspElems elems = Wasp
|
||||
{ waspElements = elems
|
||||
, waspJsImports = []
|
||||
, externalCodeFiles = []
|
||||
}
|
||||
|
||||
-- * External code files
|
||||
|
||||
getExternalCodeFiles :: Wasp -> [ExternalCode.File]
|
||||
getExternalCodeFiles = externalCodeFiles
|
||||
|
||||
setExternalCodeFiles :: Wasp -> [ExternalCode.File] -> Wasp
|
||||
setExternalCodeFiles wasp files = wasp { externalCodeFiles = files }
|
||||
|
||||
-- * Js imports
|
||||
|
||||
getJsImports :: Wasp -> [JsImport]
|
||||
getJsImports = waspJsImports
|
||||
|
||||
setJsImports :: Wasp -> [JsImport] -> Wasp
|
||||
setJsImports wasp jsImports = wasp { waspJsImports = jsImports }
|
||||
|
||||
-- * App
|
||||
|
||||
getApp :: Wasp -> App
|
||||
getApp wasp = let apps = getApps wasp in
|
||||
if (length apps /= 1)
|
||||
then error "Wasp has to contain exactly one WaspElementApp element!"
|
||||
else head apps
|
||||
|
||||
isAppElem :: WaspElement -> Bool
|
||||
isAppElem WaspElementApp{} = True
|
||||
isAppElem _ = False
|
||||
|
||||
getApps :: Wasp -> [App]
|
||||
getApps wasp = [app | (WaspElementApp app) <- waspElements wasp]
|
||||
|
||||
setApp :: Wasp -> App -> Wasp
|
||||
setApp wasp app = wasp { waspElements = (WaspElementApp app) : (filter (not . isAppElem) (waspElements wasp)) }
|
||||
|
||||
fromApp :: App -> Wasp
|
||||
fromApp app = fromWaspElems [WaspElementApp app]
|
||||
|
||||
-- * Pages
|
||||
|
||||
getPages :: Wasp -> [Page]
|
||||
getPages wasp = [page | (WaspElementPage page) <- waspElements wasp]
|
||||
|
||||
addPage :: Wasp -> Page -> Wasp
|
||||
addPage wasp page = wasp { waspElements = (WaspElementPage page):(waspElements wasp) }
|
||||
|
||||
-- * Entities
|
||||
|
||||
getEntities :: Wasp -> [Entity]
|
||||
getEntities wasp = [entity | (WaspElementEntity entity) <- (waspElements wasp)]
|
||||
|
||||
-- | Gets entity with a specified name from wasp, if such an entity exists.
|
||||
-- Entity name must be unique, so there can be no more than one such entity.
|
||||
getEntityByName :: Wasp -> String -> Maybe Entity
|
||||
getEntityByName wasp name = U.headSafe $ filter (\e -> entityName e == name) (getEntities wasp)
|
||||
|
||||
addEntity :: Wasp -> Entity -> Wasp
|
||||
addEntity wasp entity = wasp { waspElements = (WaspElementEntity entity):(waspElements wasp) }
|
||||
|
||||
-- * EntityForm
|
||||
|
||||
-- | Retrieves all entity forms for a given entity from a Wasp record.
|
||||
getEntityFormsForEntity :: Wasp -> Entity -> [EF.EntityForm]
|
||||
getEntityFormsForEntity wasp entity = filter isFormOfGivenEntity allEntityForms
|
||||
where
|
||||
allEntityForms = [entityForm | (WaspElementEntityForm entityForm) <- waspElements wasp]
|
||||
isFormOfGivenEntity ef = entityName entity == EF._entityName ef
|
||||
|
||||
-- * EntityList
|
||||
|
||||
-- | Retrieves all entity lists for a given entity from a Wasp record.
|
||||
getEntityListsForEntity :: Wasp -> Entity -> [EL.EntityList]
|
||||
getEntityListsForEntity wasp entity = filter isListOfGivenEntity allEntityLists
|
||||
where
|
||||
allEntityLists = [entityList | (WaspElementEntityList entityList) <- waspElements wasp]
|
||||
isListOfGivenEntity el = entityName entity == EL._entityName el
|
||||
|
||||
-- * ToJSON instances.
|
||||
|
||||
instance ToJSON Wasp where
|
||||
toJSON wasp = object
|
||||
[ "app" .= getApp wasp
|
||||
, "pages" .= getPages wasp
|
||||
, "jsImports" .= getJsImports wasp
|
||||
]
|
17
waspc/src/Wasp/App.hs
Normal file
17
waspc/src/Wasp/App.hs
Normal file
@ -0,0 +1,17 @@
|
||||
module Wasp.App
|
||||
( App(..)
|
||||
) where
|
||||
|
||||
import Data.Aeson ((.=), object, ToJSON(..))
|
||||
|
||||
|
||||
data App = App
|
||||
{ appName :: !String -- Identifier
|
||||
, appTitle :: !String
|
||||
} deriving (Show, Eq)
|
||||
|
||||
instance ToJSON App where
|
||||
toJSON app = object
|
||||
[ "name" .= appName app
|
||||
, "title" .= appTitle app
|
||||
]
|
41
waspc/src/Wasp/Entity.hs
Normal file
41
waspc/src/Wasp/Entity.hs
Normal file
@ -0,0 +1,41 @@
|
||||
module Wasp.Entity
|
||||
( Entity (..)
|
||||
, EntityField (..)
|
||||
, EntityFieldType (..)
|
||||
) where
|
||||
|
||||
import qualified Data.Text as Text
|
||||
import qualified Data.Aeson as Aeson
|
||||
import Data.Aeson ((.=), object, ToJSON(..))
|
||||
|
||||
|
||||
data Entity = Entity
|
||||
{ entityName :: !String
|
||||
, entityFields :: ![EntityField]
|
||||
} deriving (Show, Eq)
|
||||
|
||||
data EntityField = EntityField
|
||||
{ entityFieldName :: !String
|
||||
, entityFieldType :: !EntityFieldType
|
||||
} deriving (Show, Eq)
|
||||
|
||||
data EntityFieldType = EftString | EftBoolean deriving (Eq)
|
||||
|
||||
instance Show EntityFieldType where
|
||||
show EftString = "string"
|
||||
show EftBoolean = "boolean"
|
||||
|
||||
instance ToJSON Entity where
|
||||
toJSON entity = object
|
||||
[ "name" .= entityName entity
|
||||
, "fields" .= entityFields entity
|
||||
]
|
||||
|
||||
instance ToJSON EntityField where
|
||||
toJSON entityField = object
|
||||
[ "name" .= entityFieldName entityField
|
||||
, "type" .= entityFieldType entityField
|
||||
]
|
||||
|
||||
instance ToJSON EntityFieldType where
|
||||
toJSON = Aeson.String . Text.pack . show
|
93
waspc/src/Wasp/EntityForm.hs
Normal file
93
waspc/src/Wasp/EntityForm.hs
Normal file
@ -0,0 +1,93 @@
|
||||
module Wasp.EntityForm
|
||||
( EntityForm(..)
|
||||
, Submit(..)
|
||||
, SubmitButton(..)
|
||||
, Field(..)
|
||||
, DefaultValue(..)
|
||||
, getConfigForField
|
||||
) where
|
||||
|
||||
import Data.Aeson ((.=), object, ToJSON(..))
|
||||
|
||||
import qualified Util as U
|
||||
import qualified Wasp.Entity as Entity
|
||||
|
||||
|
||||
data EntityForm = EntityForm
|
||||
{ _name :: !String -- Name of the form
|
||||
, _entityName :: !String -- Name of the entity the form is linked to
|
||||
-- TODO(matija): should we make these maybes also strict?
|
||||
, _submit :: Maybe Submit
|
||||
, _fields :: [Field]
|
||||
} deriving (Show, Eq)
|
||||
|
||||
-- NOTE(matija): Ideally generator would not depend on this logic defined outside of it.
|
||||
-- We are moving away from this approach but some parts of code (Page generator) still
|
||||
-- rely on it so we cannot remove it completely yet without further refactoring.
|
||||
--
|
||||
-- Some record fields are note even included (e.g. _fields), we are keeping this only for the
|
||||
-- backwards compatibility.
|
||||
instance ToJSON EntityForm where
|
||||
toJSON entityForm = object
|
||||
[ "name" .= _name entityForm
|
||||
, "entityName" .= _entityName entityForm
|
||||
, "submitConfig" .= _submit entityForm
|
||||
]
|
||||
|
||||
-- | For a given entity field, returns its configuration from the given entity-form, if present.
|
||||
getConfigForField :: EntityForm -> Entity.EntityField -> Maybe Field
|
||||
getConfigForField entityForm entityField =
|
||||
U.headSafe $ filter isConfigOfInputEntityField $ _fields entityForm
|
||||
where
|
||||
isConfigOfInputEntityField :: Field -> Bool
|
||||
isConfigOfInputEntityField =
|
||||
(== Entity.entityFieldName entityField) . _fieldName
|
||||
|
||||
-- * Submit
|
||||
|
||||
data Submit = Submit
|
||||
{ _onEnter :: Maybe Bool
|
||||
, _submitButton :: Maybe SubmitButton
|
||||
} deriving (Show, Eq)
|
||||
|
||||
data SubmitButton = SubmitButton
|
||||
{ _submitButtonShow :: Maybe Bool
|
||||
} deriving (Show, Eq)
|
||||
|
||||
instance ToJSON Submit where
|
||||
toJSON submit = object
|
||||
[ "onEnter" .= _onEnter submit
|
||||
]
|
||||
|
||||
-- * Field
|
||||
|
||||
data Field = Field
|
||||
{ _fieldName :: !String
|
||||
, _fieldShow :: Maybe Bool
|
||||
, _fieldDefaultValue :: Maybe DefaultValue
|
||||
, _fieldPlaceholder :: Maybe String
|
||||
-- NOTE(matija): We use Maybe (Maybe String) here to differentiate between the 3
|
||||
-- possible states:
|
||||
--
|
||||
-- Nothing -> property not provided (by user)
|
||||
-- Just Nothing -> property was provided and explicitly set to Nothing
|
||||
-- Just (Just val) -> property was provided and explicitly set to some value.
|
||||
--
|
||||
-- We introduced this because we need to differentiate between the case when user did not
|
||||
-- provide a property (we want to display a label with a default value) and a case when user
|
||||
-- explicitly disabled the label (we want not to display the label at all).
|
||||
--
|
||||
-- This is an experiment, we are not sure if this will prove to be practical. With introducing
|
||||
-- this new type of "none", the question is where else it can be applied etc.
|
||||
--
|
||||
-- Alternative solution would be to introduce another property, e.g. "showLabel: true|false"
|
||||
-- then we would have avoided the need to introduce this new "type".
|
||||
, _fieldLabel :: Maybe (Maybe String)
|
||||
} deriving (Show, Eq)
|
||||
|
||||
data DefaultValue
|
||||
= DefaultValueString String
|
||||
| DefaultValueBool Bool
|
||||
deriving (Show, Eq)
|
||||
|
||||
|
50
waspc/src/Wasp/EntityList.hs
Normal file
50
waspc/src/Wasp/EntityList.hs
Normal file
@ -0,0 +1,50 @@
|
||||
module Wasp.EntityList
|
||||
( EntityList(..)
|
||||
, Field(..)
|
||||
, getConfigForField
|
||||
) where
|
||||
|
||||
import Data.Aeson ((.=), object, ToJSON(..))
|
||||
|
||||
import Wasp.JsCode (JsCode)
|
||||
|
||||
import qualified Util as U
|
||||
import qualified Wasp.Entity as Entity
|
||||
|
||||
|
||||
data EntityList = EntityList
|
||||
{ _name :: !String -- Name of the list
|
||||
, _entityName :: !String -- Name of the entity the form is linked to
|
||||
, _showHeader :: Maybe Bool -- Should the list header be displayed or not
|
||||
, _fields :: [Field]
|
||||
} deriving (Show, Eq)
|
||||
|
||||
-- NOTE(matija): Ideally generator would not depend on this logic defined outside of it.
|
||||
-- We are moving away from this approach but some parts of code (Page generator) still
|
||||
-- rely on it so we cannot remove it completely yet without further refactoring.
|
||||
--
|
||||
-- Some record fields are note even included (e.g. _fields), we are keeping this only for the
|
||||
-- backwards compatibility.
|
||||
instance ToJSON EntityList where
|
||||
toJSON entityList = object
|
||||
[ "name" .= _name entityList
|
||||
, "entityName" .= _entityName entityList
|
||||
]
|
||||
|
||||
-- | For a given entity field, returns its configuration from the given entity-list, if present.
|
||||
-- TODO(matija): this is very similar to the same function in EntityForm, we could extract it
|
||||
-- (prob. using typeclass or TH) in the future.
|
||||
getConfigForField :: EntityList -> Entity.EntityField -> Maybe Field
|
||||
getConfigForField entityList entityField =
|
||||
U.headSafe $ filter isConfigOfInputEntityField $ _fields entityList
|
||||
where
|
||||
isConfigOfInputEntityField :: Field -> Bool
|
||||
isConfigOfInputEntityField =
|
||||
(== Entity.entityFieldName entityField) . _fieldName
|
||||
|
||||
-- * Field
|
||||
|
||||
data Field = Field
|
||||
{ _fieldName :: !String
|
||||
, _fieldRender :: Maybe JsCode -- Js function that renders a list field.
|
||||
} deriving (Show, Eq)
|
14
waspc/src/Wasp/JsCode.hs
Normal file
14
waspc/src/Wasp/JsCode.hs
Normal file
@ -0,0 +1,14 @@
|
||||
module Wasp.JsCode
|
||||
( JsCode(..)
|
||||
) where
|
||||
|
||||
import Data.Aeson (ToJSON(..))
|
||||
import Data.Text (Text)
|
||||
|
||||
data JsCode = JsCode !Text deriving (Show, Eq)
|
||||
|
||||
-- TODO(matija): Currently generator is relying on this implementation, which is not
|
||||
-- ideal. Ideally all the generation logic would be in the generator. But for now this was
|
||||
-- the simplest way to implement it.
|
||||
instance ToJSON JsCode where
|
||||
toJSON (JsCode code) = toJSON code
|
23
waspc/src/Wasp/JsImport.hs
Normal file
23
waspc/src/Wasp/JsImport.hs
Normal file
@ -0,0 +1,23 @@
|
||||
module Wasp.JsImport
|
||||
( JsImport(..)
|
||||
) where
|
||||
|
||||
import Data.Aeson ((.=), object, ToJSON(..))
|
||||
import qualified Path.Aliases as Path
|
||||
|
||||
|
||||
-- | Represents javascript import -> "import <what> from <from>".
|
||||
data JsImport = JsImport
|
||||
{ jsImportWhat :: !String
|
||||
-- | Path of file to import, relative to external code directory.
|
||||
-- So for example if jsImportFrom is "test.js", we expect file
|
||||
-- to exist at <external_code_dir>/test.js.
|
||||
-- TODO: Make this more explicit in the code (both here and in wasp lang)? Also, support importing npm packages?
|
||||
, jsImportFrom :: !Path.RelFile
|
||||
} deriving (Show, Eq)
|
||||
|
||||
instance ToJSON JsImport where
|
||||
toJSON jsImport = object
|
||||
[ "what" .= jsImportWhat jsImport
|
||||
, "from" .= jsImportFrom jsImport
|
||||
]
|
24
waspc/src/Wasp/Page.hs
Normal file
24
waspc/src/Wasp/Page.hs
Normal file
@ -0,0 +1,24 @@
|
||||
module Wasp.Page
|
||||
( Page(..)
|
||||
) where
|
||||
|
||||
import Data.Aeson ((.=), object, ToJSON(..))
|
||||
|
||||
import qualified Wasp.Style as WStyle
|
||||
|
||||
|
||||
data Page = Page
|
||||
{ pageName :: !String
|
||||
, pageRoute :: !String
|
||||
, pageContent :: !String
|
||||
-- | TODO(martin): I did not know how to apply strictness annotation (!) here.
|
||||
, pageStyle :: Maybe WStyle.Style
|
||||
} deriving (Show, Eq)
|
||||
|
||||
instance ToJSON Page where
|
||||
toJSON page = object
|
||||
[ "name" .= pageName page
|
||||
, "route" .= pageRoute page
|
||||
, "content" .= pageContent page
|
||||
, "style" .= pageStyle page
|
||||
]
|
16
waspc/src/Wasp/Style.hs
Normal file
16
waspc/src/Wasp/Style.hs
Normal file
@ -0,0 +1,16 @@
|
||||
module Wasp.Style
|
||||
( Style(..)
|
||||
) where
|
||||
|
||||
import Data.Aeson (ToJSON(..))
|
||||
import Data.Text (Text)
|
||||
import qualified Path.Aliases as Path
|
||||
|
||||
|
||||
data Style = ExtCodeCssFile !Path.RelFile
|
||||
| CssCode !Text
|
||||
deriving (Show, Eq)
|
||||
|
||||
instance ToJSON Style where
|
||||
toJSON (ExtCodeCssFile path) = toJSON path
|
||||
toJSON (CssCode code) = toJSON code
|
65
waspc/stack.yaml
Normal file
65
waspc/stack.yaml
Normal file
@ -0,0 +1,65 @@
|
||||
# This file was automatically generated by 'stack init'
|
||||
#
|
||||
# Some commonly used options have been documented as comments in this file.
|
||||
# For advanced use and comprehensive documentation of the format, please see:
|
||||
# https://docs.haskellstack.org/en/stable/yaml_configuration/
|
||||
|
||||
# Resolver to choose a 'specific' stackage snapshot or a compiler version.
|
||||
# A snapshot resolver dictates the compiler version and the set of packages
|
||||
# to be used for project dependencies. For example:
|
||||
#
|
||||
# resolver: lts-3.5
|
||||
# resolver: nightly-2015-09-21
|
||||
# resolver: ghc-7.10.2
|
||||
# resolver: ghcjs-0.1.0_ghc-7.10.2
|
||||
#
|
||||
# The location of a snapshot can be provided as a file or url. Stack assumes
|
||||
# a snapshot provided as a file might change, whereas a url resource does not.
|
||||
#
|
||||
# resolver: ./custom-snapshot.yaml
|
||||
# resolver: https://example.com/snapshots/2018-01-01.yaml
|
||||
resolver: lts-13.7
|
||||
|
||||
# User packages to be built.
|
||||
# Various formats can be used as shown in the example below.
|
||||
#
|
||||
# packages:
|
||||
# - some-directory
|
||||
# - https://example.com/foo/bar/baz-0.0.2.tar.gz
|
||||
# - location:
|
||||
# git: https://github.com/commercialhaskell/stack.git
|
||||
# commit: e7b331f14bcffb8367cd58fbfc8b40ec7642100a
|
||||
# - location: https://github.com/commercialhaskell/stack/commit/e7b331f14bcffb8367cd58fbfc8b40ec7642100a
|
||||
# subdirs:
|
||||
# - auto-update
|
||||
# - wai
|
||||
packages:
|
||||
- .
|
||||
# Dependency packages to be pulled from upstream that are not in the resolver
|
||||
# using the same syntax as the packages field.
|
||||
# (e.g., acme-missiles-0.3)
|
||||
# extra-deps: []
|
||||
|
||||
# Override default flag values for local packages and extra-deps
|
||||
# flags: {}
|
||||
|
||||
# Extra package databases containing global packages
|
||||
# extra-package-dbs: []
|
||||
|
||||
# Control whether we use the GHC we find on the path
|
||||
# system-ghc: true
|
||||
#
|
||||
# Require a specific version of stack, using version ranges
|
||||
# require-stack-version: -any # Default
|
||||
# require-stack-version: ">=1.7"
|
||||
#
|
||||
# Override the architecture used by stack, especially useful on Windows
|
||||
# arch: i386
|
||||
# arch: x86_64
|
||||
#
|
||||
# Extra directories used by stack for building
|
||||
# extra-include-dirs: [/path/to/dir]
|
||||
# extra-lib-dirs: [/path/to/dir]
|
||||
#
|
||||
# Allow a newer minor version of GHC than the snapshot specifies
|
||||
# compiler-check: newer-minor
|
65
waspc/test/Fixtures.hs
Normal file
65
waspc/test/Fixtures.hs
Normal file
@ -0,0 +1,65 @@
|
||||
module Fixtures where
|
||||
|
||||
import Wasp
|
||||
import qualified Wasp.EntityForm as EF
|
||||
import qualified Generator.Entity.EntityForm as GEF
|
||||
|
||||
app :: App
|
||||
app = App
|
||||
{ appName = "test_app"
|
||||
, appTitle = "Hello World!"
|
||||
}
|
||||
|
||||
taskEntity :: Entity
|
||||
taskEntity = Entity
|
||||
{ entityName = "Task"
|
||||
, entityFields =
|
||||
[ Wasp.EntityField "description" Wasp.EftString
|
||||
, Wasp.EntityField "isDone" Wasp.EftBoolean
|
||||
]
|
||||
}
|
||||
|
||||
taskCreateForm :: EF.EntityForm
|
||||
taskCreateForm = EF.EntityForm
|
||||
{ EF._name = "CreateTaskForm"
|
||||
, EF._entityName = "Task"
|
||||
, EF._submit = Just EF.Submit
|
||||
{ EF._onEnter = Just False
|
||||
, EF._submitButton = Nothing
|
||||
}
|
||||
, EF._fields = []
|
||||
}
|
||||
|
||||
userEntity :: Entity
|
||||
userEntity = Entity
|
||||
{ entityName = "User"
|
||||
, entityFields =
|
||||
[ Wasp.EntityField "name" Wasp.EftString
|
||||
, Wasp.EntityField "surname" Wasp.EftString
|
||||
]
|
||||
}
|
||||
|
||||
userCreateForm :: EF.EntityForm
|
||||
userCreateForm = EF.EntityForm
|
||||
{ EF._name = "CreateUserForm"
|
||||
, EF._entityName = "User"
|
||||
, EF._submit = Nothing
|
||||
, EF._fields = []
|
||||
}
|
||||
|
||||
wasp :: Wasp
|
||||
wasp = fromWaspElems
|
||||
[ WaspElementApp app
|
||||
, WaspElementEntity taskEntity
|
||||
, WaspElementEntityForm taskCreateForm
|
||||
]
|
||||
|
||||
formFieldIsDone :: GEF.FormFieldTemplateData
|
||||
formFieldIsDone = GEF.FormFieldTemplateData
|
||||
{ GEF._fieldName = "isDone"
|
||||
, GEF._fieldType = Wasp.EftBoolean
|
||||
, GEF._fieldShow = True
|
||||
, GEF._fieldDefaultValue = EF.DefaultValueBool True
|
||||
, GEF._fieldPlaceholder = Nothing
|
||||
, GEF._fieldLabel = Nothing
|
||||
}
|
40
waspc/test/Generator/Entity/EntityFormTest.hs
Normal file
40
waspc/test/Generator/Entity/EntityFormTest.hs
Normal file
@ -0,0 +1,40 @@
|
||||
module Generator.Entity.EntityFormTest where
|
||||
|
||||
import Test.Tasty.Hspec
|
||||
|
||||
import Path ((</>))
|
||||
|
||||
import Wasp
|
||||
import Generator.FileDraft
|
||||
import qualified Generator.FileDraft.TemplateFileDraft as TmplFD
|
||||
import Generator.Entity.EntityForm
|
||||
import qualified Generator.Common as Common
|
||||
|
||||
import qualified Fixtures as F
|
||||
|
||||
spec_generateEntityCreateForm :: Spec
|
||||
spec_generateEntityCreateForm = do
|
||||
let waspWithTask = fromWaspElems
|
||||
[ WaspElementApp F.app
|
||||
, WaspElementEntity F.taskEntity
|
||||
, WaspElementEntityForm F.taskCreateForm
|
||||
]
|
||||
|
||||
{-
|
||||
-- NOTE(matija): Could not get this to work - the problem was that FileDraft has to be the
|
||||
-- instance of NFData typeclass so it can be fully evaulated, I did not want to go further
|
||||
-- into that.
|
||||
it "When given entity form for which there is no entity in wasp, throws an error." $ do
|
||||
(evaluate . force) (generateEntityCreateForm waspWithTask F.userCreateForm)
|
||||
`shouldThrow`
|
||||
anyErrorCall
|
||||
-}
|
||||
|
||||
it "When given entity form for which there is an entity in wasp, \
|
||||
\returns correct file draft." $ do
|
||||
let (FileDraftTemplateFd templateFileDraft) =
|
||||
generateEntityCreateForm waspWithTask F.taskCreateForm
|
||||
|
||||
let expectedDstPath = Common.srcDirPath </> (entityCreateFormPathInSrc F.taskEntity F.taskCreateForm)
|
||||
|
||||
TmplFD._dstPath templateFileDraft `shouldBe` expectedDstPath
|
40
waspc/test/Generator/EntityTest.hs
Normal file
40
waspc/test/Generator/EntityTest.hs
Normal file
@ -0,0 +1,40 @@
|
||||
module Generator.EntityTest where
|
||||
|
||||
import Test.Tasty.Hspec
|
||||
|
||||
import Path ((</>), relfile)
|
||||
|
||||
import Wasp
|
||||
import Generator.FileDraft
|
||||
import Generator.FileDraft.TemplateFileDraft
|
||||
import Generator.Entity
|
||||
|
||||
|
||||
spec_EntityGenerator :: Spec
|
||||
spec_EntityGenerator = do
|
||||
let testApp = (App "TestApp" "Test App")
|
||||
let testEntity = (Entity "TestEntity" [EntityField "testField" EftString])
|
||||
let testWasp = (fromApp testApp) `addEntity` testEntity
|
||||
|
||||
describe "generateEntityClass" $ do
|
||||
testGeneratorUsesCorrectSrcPath
|
||||
testWasp generateEntityClass (entityTemplatesDirPath </> [relfile|_Entity.js|])
|
||||
|
||||
describe "generateEntityActions" $ do
|
||||
testGeneratorUsesCorrectSrcPath
|
||||
testWasp generateEntityActions (entityTemplatesDirPath </> [relfile|actions.js|])
|
||||
|
||||
describe "generateEntityActionTypes" $ do
|
||||
testGeneratorUsesCorrectSrcPath
|
||||
testWasp generateEntityActionTypes (entityTemplatesDirPath </> [relfile|actionTypes.js|])
|
||||
|
||||
describe "generateEntityState" $ do
|
||||
testGeneratorUsesCorrectSrcPath
|
||||
testWasp generateEntityState (entityTemplatesDirPath </> [relfile|state.js|])
|
||||
|
||||
where
|
||||
testGeneratorUsesCorrectSrcPath testWasp generator expectedSrcPath = it
|
||||
"Given a simple Wasp, creates template file draft with correct src path" $ do
|
||||
let (FileDraftTemplateFd (TemplateFileDraft _ srcPath _))
|
||||
= generator testWasp (head $ getEntities testWasp)
|
||||
srcPath `shouldBe` expectedSrcPath
|
19
waspc/test/Generator/ExternalCode/JsTest.hs
Normal file
19
waspc/test/Generator/ExternalCode/JsTest.hs
Normal file
@ -0,0 +1,19 @@
|
||||
module Generator.ExternalCode.JsTest where
|
||||
|
||||
import Test.Tasty.Hspec
|
||||
|
||||
import Path (relfile)
|
||||
|
||||
import Generator.ExternalCode.Js
|
||||
|
||||
|
||||
spec_resolveJsFileWaspImports :: Spec
|
||||
spec_resolveJsFileWaspImports = do
|
||||
([relfile|ext-src/extFile.js|], "import foo from 'bar'") ~> "import foo from 'bar'"
|
||||
([relfile|ext-src/extFile.js|], "import foo from '@wasp/bar'") ~> "import foo from '../bar'"
|
||||
([relfile|ext-src/a/extFile.js|], "import foo from \"@wasp/bar/foo\"") ~>
|
||||
"import foo from \"../../bar/foo\""
|
||||
where
|
||||
(path, text) ~> expectedText =
|
||||
it ((show path) ++ " " ++ (show text) ++ " -> " ++ (show expectedText)) $ do
|
||||
resolveJsFileWaspImports path text `shouldBe` expectedText
|
27
waspc/test/Generator/FileDraft/CopyFileDraftTest.hs
Normal file
27
waspc/test/Generator/FileDraft/CopyFileDraftTest.hs
Normal file
@ -0,0 +1,27 @@
|
||||
module Generator.FileDraft.CopyFileDraftTest where
|
||||
|
||||
import Test.Tasty.Hspec
|
||||
|
||||
import qualified Path
|
||||
import Path ((</>), absdir, relfile, absfile)
|
||||
|
||||
import Generator.FileDraft
|
||||
|
||||
import qualified Generator.MockWriteableMonad as Mock
|
||||
|
||||
|
||||
spec_CopyFileDraft :: Spec
|
||||
spec_CopyFileDraft = do
|
||||
describe "write" $ do
|
||||
it "Creates new file by copying existing file" $ do
|
||||
let mock = write dstDir fileDraft
|
||||
let mockLogs = Mock.getMockLogs mock Mock.defaultMockConfig
|
||||
Mock.createDirectoryIfMissing_calls mockLogs
|
||||
`shouldBe` [(True, Path.toFilePath $ Path.parent expectedDstPath)]
|
||||
Mock.copyFile_calls mockLogs
|
||||
`shouldBe` [(Path.toFilePath expectedSrcPath, Path.toFilePath expectedDstPath)]
|
||||
where
|
||||
(dstDir, dstPath, srcPath) = ([absdir|/a/b|], [relfile|c/d/dst.txt|], [absfile|/e/src.txt|])
|
||||
fileDraft = createCopyFileDraft dstPath srcPath
|
||||
expectedSrcPath = srcPath
|
||||
expectedDstPath = dstDir </> dstPath
|
37
waspc/test/Generator/FileDraft/TemplateFileDraftTest.hs
Normal file
37
waspc/test/Generator/FileDraft/TemplateFileDraftTest.hs
Normal file
@ -0,0 +1,37 @@
|
||||
module Generator.FileDraft.TemplateFileDraftTest where
|
||||
|
||||
import Test.Tasty.Hspec
|
||||
|
||||
import Data.Aeson (object, (.=))
|
||||
import Data.Text (Text)
|
||||
import qualified Path
|
||||
import Path ((</>), absdir, relfile)
|
||||
|
||||
import Generator.FileDraft
|
||||
|
||||
import qualified Generator.MockWriteableMonad as Mock
|
||||
|
||||
|
||||
spec_TemplateFileDraft :: Spec
|
||||
spec_TemplateFileDraft = do
|
||||
describe "write" $ do
|
||||
it "Creates new file from existing template file" $ do
|
||||
let mock = write dstDir fileDraft
|
||||
let mockLogs = Mock.getMockLogs mock mockConfig
|
||||
Mock.compileAndRenderTemplate_calls mockLogs
|
||||
`shouldBe` [(templatePath, templateData)]
|
||||
Mock.createDirectoryIfMissing_calls mockLogs
|
||||
`shouldBe` [(True, Path.toFilePath $ Path.parent expectedDstPath)]
|
||||
Mock.writeFileFromText_calls mockLogs
|
||||
`shouldBe` [(Path.toFilePath expectedDstPath, mockTemplateContent)]
|
||||
where
|
||||
(dstDir, dstPath, templatePath) = ([absdir|/a/b|], [relfile|c/d/dst.txt|], [relfile|e/tmpl.txt|])
|
||||
templateData = object [ "foo" .= ("bar" :: String) ]
|
||||
fileDraft = createTemplateFileDraft dstPath templatePath (Just templateData)
|
||||
expectedDstPath = dstDir </> dstPath
|
||||
mockTemplatesDirAbsPath = [absdir|/mock/templates/dir|]
|
||||
mockTemplateContent = "Mock template content" :: Text
|
||||
mockConfig = Mock.defaultMockConfig
|
||||
{ Mock.getTemplatesDirAbsPath_impl = mockTemplatesDirAbsPath
|
||||
, Mock.compileAndRenderTemplate_impl = \_ _ -> mockTemplateContent
|
||||
}
|
83
waspc/test/Generator/GeneratorsTest.hs
Normal file
83
waspc/test/Generator/GeneratorsTest.hs
Normal file
@ -0,0 +1,83 @@
|
||||
module Generator.GeneratorsTest where
|
||||
|
||||
import Test.Tasty.Hspec
|
||||
|
||||
import System.FilePath ((</>), (<.>))
|
||||
import Path (absdir)
|
||||
import qualified Path
|
||||
|
||||
import Util
|
||||
import qualified CompileOptions
|
||||
import Generator.Generators
|
||||
import Generator.FileDraft
|
||||
import qualified Generator.FileDraft.TemplateFileDraft as TmplFD
|
||||
import qualified Generator.FileDraft.CopyFileDraft as CopyFD
|
||||
import qualified Generator.FileDraft.TextFileDraft as TextFD
|
||||
import qualified Generator.Common as Common
|
||||
import Wasp
|
||||
|
||||
-- TODO(martin): We could define Arbitrary instance for Wasp, define properties over
|
||||
-- generator functions and then do property testing on them, that would be cool.
|
||||
|
||||
spec_Generators :: Spec
|
||||
spec_Generators = do
|
||||
let testApp = (App "TestApp" "Test App")
|
||||
let testPage = (Page "TestPage" "/test-page" "<div>Test Page</div>" Nothing)
|
||||
let testEntity = (Entity "TestEntity" [EntityField "testField" EftString])
|
||||
let testWasp = (fromApp testApp) `addPage` testPage `addEntity` testEntity
|
||||
let testCompileOptions = CompileOptions.CompileOptions
|
||||
{ CompileOptions.externalCodeDirPath = [absdir|/test/src|]
|
||||
}
|
||||
|
||||
describe "generateWebApp" $ do
|
||||
-- NOTE: This test does not (for now) check that content of files is correct or
|
||||
-- that they will successfully be written, it checks only that their
|
||||
-- destinations are correct.
|
||||
it "Given a simple Wasp, creates file drafts at expected destinations" $ do
|
||||
let fileDrafts = generateWebApp testWasp testCompileOptions
|
||||
let testEntityDstDirInSrc
|
||||
= "entities" </> (Util.camelToKebabCase (entityName testEntity))
|
||||
let expectedFileDraftDstPaths = concat $
|
||||
[ [ "README.md"
|
||||
, "package.json"
|
||||
, ".gitignore"
|
||||
]
|
||||
, map ("public" </>)
|
||||
[ "favicon.ico"
|
||||
, "index.html"
|
||||
, "manifest.json"
|
||||
]
|
||||
, map ((Path.toFilePath Common.srcDirPath) </>)
|
||||
[ "logo.png"
|
||||
, "index.css"
|
||||
, "index.js"
|
||||
, "reducers.js"
|
||||
, "router.js"
|
||||
, "serviceWorker.js"
|
||||
, (pageName testPage <.> "js")
|
||||
, "store/index.js"
|
||||
, "store/middleware/logger.js"
|
||||
, testEntityDstDirInSrc </> "actions.js"
|
||||
, testEntityDstDirInSrc </> "actionTypes.js"
|
||||
, testEntityDstDirInSrc </> "state.js"
|
||||
]
|
||||
]
|
||||
|
||||
mapM_
|
||||
-- NOTE(martin): I added fd to the pair here in order to have it
|
||||
-- printed when shouldBe fails, otherwise I could not know which
|
||||
-- file draft failed.
|
||||
(\dstPath -> (dstPath, existsFdWithDst fileDrafts dstPath)
|
||||
`shouldBe` (dstPath, True))
|
||||
expectedFileDraftDstPaths
|
||||
|
||||
|
||||
existsFdWithDst :: [FileDraft] -> FilePath -> Bool
|
||||
existsFdWithDst fds dstPath = any ((== dstPath) . getFileDraftDstPath) fds
|
||||
|
||||
-- TODO(martin): This should really become part of the Writeable typeclass,
|
||||
-- since it is smth we want to do for all file drafts.
|
||||
getFileDraftDstPath :: FileDraft -> FilePath
|
||||
getFileDraftDstPath (FileDraftTemplateFd fd) = Path.toFilePath $ TmplFD._dstPath fd
|
||||
getFileDraftDstPath (FileDraftCopyFd fd) = Path.toFilePath $ CopyFD._dstPath fd
|
||||
getFileDraftDstPath (FileDraftTextFd fd) = Path.toFilePath $ TextFD._dstPath fd
|
108
waspc/test/Generator/MockWriteableMonad.hs
Normal file
108
waspc/test/Generator/MockWriteableMonad.hs
Normal file
@ -0,0 +1,108 @@
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
|
||||
module Generator.MockWriteableMonad
|
||||
( MockWriteableMonad
|
||||
, MockWriteableMonadLogs(..)
|
||||
, MockWriteableMonadConfig(..)
|
||||
, getMockLogs
|
||||
, defaultMockConfig
|
||||
) where
|
||||
|
||||
import Data.Text (Text, pack)
|
||||
import Control.Monad.State
|
||||
import qualified Data.Aeson as Aeson
|
||||
import Path ((</>), absdir)
|
||||
import qualified Path.Aliases as Path
|
||||
|
||||
import Generator.FileDraft.WriteableMonad
|
||||
|
||||
|
||||
-- TODO: Instead of manually defining mock like this, consider using monad-mock package,
|
||||
-- it should do most of this automatically, now there is a lot of boilerplate.
|
||||
-- Or we ourselves can maybe use template haskell to reduce duplication.
|
||||
|
||||
defaultMockConfig :: MockWriteableMonadConfig
|
||||
defaultMockConfig = MockWriteableMonadConfig
|
||||
{ getTemplatesDirAbsPath_impl = [absdir|/mock/templates/dir|]
|
||||
, getTemplateFileAbsPath_impl = \path -> [absdir|/mock/templates/dir|] </> path
|
||||
, compileAndRenderTemplate_impl = \_ _ -> (pack "Mock template content")
|
||||
}
|
||||
|
||||
getMockLogs :: MockWriteableMonad a -> MockWriteableMonadConfig -> MockWriteableMonadLogs
|
||||
getMockLogs mock config = fst $ execState (unMockWriteableMonad mock) (emptyLogs, config)
|
||||
where
|
||||
emptyLogs = MockWriteableMonadLogs [] [] [] [] [] []
|
||||
|
||||
instance WriteableMonad MockWriteableMonad where
|
||||
writeFileFromText dstPath text = MockWriteableMonad $ do
|
||||
modifyLogs (writeFileFromText_addCall dstPath text)
|
||||
|
||||
getTemplatesDirAbsPath = MockWriteableMonad $ do
|
||||
modifyLogs getTemplatesDirAbsPath_addCall
|
||||
(_, config) <- get
|
||||
return $ getTemplatesDirAbsPath_impl config
|
||||
|
||||
createDirectoryIfMissing createParents path = MockWriteableMonad $ do
|
||||
modifyLogs (createDirectoryIfMissing_addCall createParents path)
|
||||
|
||||
copyFile srcPath dstPath = MockWriteableMonad $ do
|
||||
modifyLogs (copyFile_addCall srcPath dstPath)
|
||||
|
||||
getTemplateFileAbsPath path = MockWriteableMonad $ do
|
||||
modifyLogs (getTemplateFileAbsPath_addCall path)
|
||||
(_, config) <- get
|
||||
return $ (getTemplateFileAbsPath_impl config) path
|
||||
|
||||
compileAndRenderTemplate path json = MockWriteableMonad $ do
|
||||
modifyLogs (compileAndRenderTemplate_addCall path json)
|
||||
(_, config) <- get
|
||||
return $ (compileAndRenderTemplate_impl config) path json
|
||||
|
||||
modifyLogs :: MonadState (a, b) m => (a -> a) -> m ()
|
||||
modifyLogs f = modify (\(logs, config) -> (f logs, config))
|
||||
|
||||
newtype MockWriteableMonad a = MockWriteableMonad
|
||||
{ unMockWriteableMonad :: State (MockWriteableMonadLogs, MockWriteableMonadConfig) a
|
||||
}
|
||||
deriving (Monad, Applicative, Functor)
|
||||
|
||||
data MockWriteableMonadLogs = MockWriteableMonadLogs
|
||||
{ writeFileFromText_calls :: [(FilePath, Text)]
|
||||
, getTemplatesDirAbsPath_calls :: [()]
|
||||
, createDirectoryIfMissing_calls :: [(Bool, FilePath)]
|
||||
, copyFile_calls :: [(FilePath, FilePath)]
|
||||
, getTemplateFileAbsPath_calls :: [(Path.RelFile)]
|
||||
, compileAndRenderTemplate_calls :: [(Path.RelFile, Aeson.Value)]
|
||||
}
|
||||
|
||||
data MockWriteableMonadConfig = MockWriteableMonadConfig
|
||||
{ getTemplatesDirAbsPath_impl :: Path.AbsDir
|
||||
, getTemplateFileAbsPath_impl :: Path.RelFile -> Path.AbsFile
|
||||
, compileAndRenderTemplate_impl :: Path.RelFile -> Aeson.Value -> Text
|
||||
}
|
||||
|
||||
writeFileFromText_addCall :: FilePath -> Text -> MockWriteableMonadLogs -> MockWriteableMonadLogs
|
||||
writeFileFromText_addCall path text logs =
|
||||
logs { writeFileFromText_calls = (path, text):(writeFileFromText_calls logs) }
|
||||
|
||||
getTemplatesDirAbsPath_addCall :: MockWriteableMonadLogs -> MockWriteableMonadLogs
|
||||
getTemplatesDirAbsPath_addCall logs =
|
||||
logs { getTemplatesDirAbsPath_calls = ():(getTemplatesDirAbsPath_calls logs) }
|
||||
|
||||
getTemplateFileAbsPath_addCall :: Path.RelFile -> MockWriteableMonadLogs -> MockWriteableMonadLogs
|
||||
getTemplateFileAbsPath_addCall path logs =
|
||||
logs { getTemplateFileAbsPath_calls = (path):(getTemplateFileAbsPath_calls logs) }
|
||||
|
||||
copyFile_addCall :: FilePath -> FilePath -> MockWriteableMonadLogs -> MockWriteableMonadLogs
|
||||
copyFile_addCall srcPath dstPath logs =
|
||||
logs { copyFile_calls = (srcPath, dstPath):(copyFile_calls logs) }
|
||||
|
||||
createDirectoryIfMissing_addCall :: Bool -> FilePath -> MockWriteableMonadLogs -> MockWriteableMonadLogs
|
||||
createDirectoryIfMissing_addCall createParents path logs =
|
||||
logs { createDirectoryIfMissing_calls =
|
||||
(createParents, path):(createDirectoryIfMissing_calls logs) }
|
||||
|
||||
compileAndRenderTemplate_addCall :: Path.RelFile -> Aeson.Value -> MockWriteableMonadLogs -> MockWriteableMonadLogs
|
||||
compileAndRenderTemplate_addCall path json logs =
|
||||
logs { compileAndRenderTemplate_calls =
|
||||
(path, json):(compileAndRenderTemplate_calls logs) }
|
23
waspc/test/Generator/PageGeneratorTest.hs
Normal file
23
waspc/test/Generator/PageGeneratorTest.hs
Normal file
@ -0,0 +1,23 @@
|
||||
module Generator.PageGeneratorTest where
|
||||
|
||||
import Test.Tasty.Hspec
|
||||
|
||||
import Path (relfile)
|
||||
|
||||
import Wasp
|
||||
import Generator.FileDraft
|
||||
import Generator.FileDraft.TemplateFileDraft
|
||||
import Generator.PageGenerator
|
||||
|
||||
|
||||
spec_PageGenerator :: Spec
|
||||
spec_PageGenerator = do
|
||||
let testApp = (App "TestApp" "Test App")
|
||||
let testPage = (Page "TestPage" "/test-page" "<div>Test Page</div>" Nothing)
|
||||
let testWasp = (fromApp testApp) `addPage` testPage
|
||||
|
||||
describe "generatePageComponent" $ do
|
||||
it "Given a simple Wasp, creates template file draft from _Page.js" $ do
|
||||
let (FileDraftTemplateFd (TemplateFileDraft _ srcPath _))
|
||||
= generatePageComponent testWasp (head $ getPages testWasp)
|
||||
srcPath `shouldBe` [relfile|src/_Page.js|]
|
100
waspc/test/Parser/CommonTest.hs
Normal file
100
waspc/test/Parser/CommonTest.hs
Normal file
@ -0,0 +1,100 @@
|
||||
module Parser.CommonTest where
|
||||
|
||||
import Test.Tasty.Hspec
|
||||
|
||||
import Text.Parsec
|
||||
import Data.Either
|
||||
import Path (relfile)
|
||||
|
||||
import Lexer
|
||||
import Parser.Common
|
||||
|
||||
spec_parseWaspCommon :: Spec
|
||||
spec_parseWaspCommon = do
|
||||
describe "Parsing wasp element linked to an entity" $ do
|
||||
it "When given a valid declaration, parses it correctly." $ do
|
||||
runWaspParser (waspElementLinkedToEntity "entity-form" whiteSpace) "entity-form<Task> TaskForm { }"
|
||||
`shouldBe` Right ("Task", "TaskForm", ())
|
||||
|
||||
describe "Parsing wasp element name and properties" $ do
|
||||
let parseWaspElementNameAndClosure elemKeyword p input =
|
||||
runWaspParser (waspElementNameAndClosure elemKeyword p) input
|
||||
|
||||
it "When given valid wasp element declaration along with whitespace parser,\
|
||||
\ returns an expected result" $ do
|
||||
parseWaspElementNameAndClosure "app" whiteSpace "app someApp { }"
|
||||
`shouldBe` Right ("someApp", ())
|
||||
|
||||
it "When given valid wasp element declaration along with char parser, returns\
|
||||
\ an expected result" $ do
|
||||
parseWaspElementNameAndClosure "app" (char 'a') "app someApp {a}"
|
||||
`shouldBe` Right ("someApp", 'a')
|
||||
|
||||
it "When given wasp element declaration with invalid name, returns Left" $ do
|
||||
(isLeft $ parseWaspElementNameAndClosure "app" whiteSpace "app 1someApp { }")
|
||||
`shouldBe` True
|
||||
|
||||
describe "Parsing wasp closure" $ do
|
||||
it "Parses a closure with braces {}" $ do
|
||||
runWaspParser (waspClosure (symbol "content")) "{ content }"
|
||||
`shouldBe` Right "content"
|
||||
|
||||
it "Does not parse a closure with brackets []" $ do
|
||||
(isLeft $ runWaspParser (waspClosure (symbol "content")) "[ content ]")
|
||||
`shouldBe` True
|
||||
|
||||
describe "Parsing wasp property with a closure as a value" $ do
|
||||
it "When given a string as a key and closure as a value, returns closure content." $ do
|
||||
runWaspParser (waspPropertyClosure "someKey" (symbol "content")) "someKey: { content }"
|
||||
`shouldBe` Right "content"
|
||||
|
||||
describe "Parsing wasp property - string literal" $ do
|
||||
let parseWaspPropertyStringLiteral key input =
|
||||
runWaspParser (waspPropertyStringLiteral key) input
|
||||
|
||||
it "When given key/value with int value, returns Left." $ do
|
||||
isLeft (parseWaspPropertyStringLiteral "title" "title: 23")
|
||||
`shouldBe` True
|
||||
|
||||
it "When given key/value with string value, returns a parsed value." $ do
|
||||
let appTitle = "my first app"
|
||||
parseWaspPropertyStringLiteral "title" ("title: \"" ++ appTitle ++ "\"")
|
||||
`shouldBe` Right appTitle
|
||||
|
||||
describe "Parsing wasp property - jsx closure {=jsx...jsx=}" $ do
|
||||
let parseWaspPropertyJsxClosure key input =
|
||||
runWaspParser (waspPropertyJsxClosure key) input
|
||||
|
||||
it "When given unexpected property key, returns Left." $ do
|
||||
isLeft (parseWaspPropertyJsxClosure "content" "title: 23")
|
||||
`shouldBe` True
|
||||
|
||||
it "When given content within jsx closure, returns that content." $ do
|
||||
parseWaspPropertyJsxClosure "content" "content: {=jsx some content jsx=}"
|
||||
`shouldBe` Right "some content"
|
||||
|
||||
describe "Parsing wasp jsx closure" $ do
|
||||
let parseWaspJsxClosure input = runWaspParser waspJsxClosure input
|
||||
let closureContent = "<div>hello world</div>"
|
||||
|
||||
it "Returns the content of closure" $ do
|
||||
parseWaspJsxClosure ("{=jsx " ++ closureContent ++ " jsx=}")
|
||||
`shouldBe` Right closureContent
|
||||
|
||||
it "Can parse braces {} within the closure" $ do
|
||||
let closureContentWithBraces = "<div>hello world {task.length}</div>"
|
||||
|
||||
parseWaspJsxClosure ("{=jsx " ++ closureContentWithBraces ++ " jsx=}")
|
||||
`shouldBe` Right closureContentWithBraces
|
||||
|
||||
it "Removes leading and trailing spaces" $ do
|
||||
parseWaspJsxClosure ("{=jsx " ++ closureContent ++ " jsx=}")
|
||||
`shouldBe` Right closureContent
|
||||
|
||||
describe "Parsing relative file path string" $ do
|
||||
it "Correctly parses relative path in double quotes" $ do
|
||||
runWaspParser relFilePathString "\"foo/bar.txt\""
|
||||
`shouldBe` Right [relfile|foo/bar.txt|]
|
||||
|
||||
it "When path is not relative, returns Left" $ do
|
||||
isLeft (runWaspParser relFilePathString "\"/foo/bar.txt\"") `shouldBe` True
|
55
waspc/test/Parser/EntityFormTest.hs
Normal file
55
waspc/test/Parser/EntityFormTest.hs
Normal file
@ -0,0 +1,55 @@
|
||||
module Parser.EntityFormTest where
|
||||
|
||||
import Test.Tasty.Hspec
|
||||
|
||||
import Parser.Common (runWaspParser)
|
||||
import Parser.Entity.EntityForm
|
||||
( entityForm
|
||||
, submitConfig
|
||||
|
||||
, entityFormOptionSubmit
|
||||
, EntityFormOption (..)
|
||||
)
|
||||
import qualified Wasp.EntityForm as EF
|
||||
|
||||
spec_parseEntityForm :: Spec
|
||||
spec_parseEntityForm = do
|
||||
|
||||
describe "Parsing Wasp element entity-form" $ do
|
||||
it "When given an entity form with submit config, it is included in the result." $ do
|
||||
runWaspParser entityForm "entity-form<Task> someEntityForm { submit: {onEnter: true} }"
|
||||
`shouldBe` Right EF.EntityForm
|
||||
{ EF._name = "someEntityForm"
|
||||
, EF._entityName = "Task"
|
||||
, EF._submit = Just (EF.Submit
|
||||
{ EF._onEnter = Just True
|
||||
, EF._submitButton = Nothing
|
||||
}
|
||||
)
|
||||
, EF._fields = []
|
||||
}
|
||||
|
||||
it "When given an entity form without submit config, it is not included in the result." $ do
|
||||
runWaspParser entityForm "entity-form<Task> someEntityForm {}"
|
||||
`shouldBe` Right EF.EntityForm
|
||||
{ EF._name = "someEntityForm"
|
||||
, EF._entityName = "Task"
|
||||
, EF._submit = Nothing
|
||||
, EF._fields = []
|
||||
}
|
||||
|
||||
describe "Parsing submit option - submit: {...}" $ do
|
||||
it "When given a valid submit option declaration, parses it correctly." $ do
|
||||
runWaspParser entityFormOptionSubmit "submit: { onEnter: true }"
|
||||
`shouldBe` Right (EfoSubmit EF.Submit
|
||||
{ EF._onEnter = Just True
|
||||
, EF._submitButton = Nothing
|
||||
})
|
||||
|
||||
describe "Parsing submit config - closure content" $ do
|
||||
it "When given a valid submit configuration, parses it correctly." $ do
|
||||
runWaspParser submitConfig "onEnter: true"
|
||||
`shouldBe` Right EF.Submit
|
||||
{ EF._onEnter = Just True
|
||||
, EF._submitButton = Nothing
|
||||
}
|
58
waspc/test/Parser/EntityTest.hs
Normal file
58
waspc/test/Parser/EntityTest.hs
Normal file
@ -0,0 +1,58 @@
|
||||
module Parser.EntityTest where
|
||||
|
||||
import Test.Tasty.Hspec
|
||||
|
||||
import Data.Either (isLeft)
|
||||
|
||||
import Parser.Common (runWaspParser)
|
||||
import Parser.Entity
|
||||
( entityField
|
||||
, entityFields
|
||||
, entity
|
||||
, entityFieldType
|
||||
)
|
||||
import qualified Wasp
|
||||
|
||||
|
||||
spec_parseEntity :: Spec
|
||||
spec_parseEntity = do
|
||||
let titleStringField = Wasp.EntityField "title" Wasp.EftString
|
||||
let isDoneBoolField = Wasp.EntityField "isDone" Wasp.EftBoolean
|
||||
|
||||
describe "Parsing entity" $ do
|
||||
it "When given a valid entity declaration, parses it correctly" $ do
|
||||
runWaspParser entity "entity Task { title :: string, isDone :: boolean }"
|
||||
`shouldBe` Right Wasp.Entity
|
||||
{ Wasp.entityName = "Task"
|
||||
, Wasp.entityFields = [titleStringField, isDoneBoolField]
|
||||
}
|
||||
|
||||
describe "Parsing entity fields separated with a comma" $ do
|
||||
it "When given multiple comma-separated fields, parses them correctly." $ do
|
||||
runWaspParser entityFields "title :: string, isDone :: boolean"
|
||||
`shouldBe` Right
|
||||
[ titleStringField
|
||||
, isDoneBoolField
|
||||
]
|
||||
|
||||
describe "Parsing entity field declaration" $ do
|
||||
it "When given a valid field declaration, parses it correctly" $ do
|
||||
runWaspParser entityField "title :: string"
|
||||
`shouldBe` Right (Wasp.EntityField "title" Wasp.EftString)
|
||||
|
||||
it "When given an invalid field declaration, returns Left" $ do
|
||||
isLeft (runWaspParser entityField "title <>")
|
||||
`shouldBe` True
|
||||
|
||||
describe "Parsing entity field type declaration" $ do
|
||||
it "When given a field type of string, parses it correctly" $ do
|
||||
runWaspParser entityFieldType "string"
|
||||
`shouldBe` Right Wasp.EftString
|
||||
|
||||
it "When given a field type of boolean, parses it correctly" $ do
|
||||
runWaspParser entityFieldType "boolean"
|
||||
`shouldBe` Right Wasp.EftBoolean
|
||||
|
||||
it "When given an unknown field type, returns Left" $ do
|
||||
isLeft (runWaspParser entityFieldType "unknownType")
|
||||
`shouldBe` True
|
20
waspc/test/Parser/ExternalCodeTest.hs
Normal file
20
waspc/test/Parser/ExternalCodeTest.hs
Normal file
@ -0,0 +1,20 @@
|
||||
module Parser.ExternalCodeTest where
|
||||
|
||||
import Test.Tasty.Hspec
|
||||
|
||||
import Data.Either (isLeft)
|
||||
import Path (relfile)
|
||||
|
||||
import Parser.ExternalCode (extCodeFilePathString)
|
||||
import Parser.Common (runWaspParser)
|
||||
|
||||
|
||||
spec_ParserExternalCode :: Spec
|
||||
spec_ParserExternalCode = do
|
||||
describe "Parsing external code file path string" $ do
|
||||
it "Correctly parses external code path in double quotes" $ do
|
||||
runWaspParser extCodeFilePathString "\"@ext/foo/bar.txt\""
|
||||
`shouldBe` Right [relfile|foo/bar.txt|]
|
||||
|
||||
it "When path does not start with @ext/, returns Left" $ do
|
||||
isLeft (runWaspParser extCodeFilePathString "\"@ext2/foo/bar.txt\"") `shouldBe` True
|
37
waspc/test/Parser/JsImportTest.hs
Normal file
37
waspc/test/Parser/JsImportTest.hs
Normal file
@ -0,0 +1,37 @@
|
||||
module Parser.JsImportTest where
|
||||
|
||||
import Test.Tasty.Hspec
|
||||
|
||||
import Data.Either (isLeft)
|
||||
import Path (relfile)
|
||||
|
||||
import Parser.Common (runWaspParser)
|
||||
import Parser.JsImport (jsImport)
|
||||
import qualified Wasp
|
||||
|
||||
|
||||
spec_parseJsImport :: Spec
|
||||
spec_parseJsImport = do
|
||||
it "Parses external code js import correctly" $ do
|
||||
runWaspParser jsImport "import something from \"@ext/some/file.js\""
|
||||
`shouldBe` Right (Wasp.JsImport "something" [relfile|some/file.js|])
|
||||
|
||||
it "Parses correctly when there is whitespace up front" $ do
|
||||
runWaspParser jsImport " import something from \"@ext/some/file.js\""
|
||||
`shouldBe` Right (Wasp.JsImport "something" [relfile|some/file.js|])
|
||||
|
||||
it "Parses correctly when 'from' is part of WHAT part" $ do
|
||||
runWaspParser jsImport "import somethingfrom from \"@ext/some/file.js\""
|
||||
`shouldBe` Right (Wasp.JsImport "somethingfrom" [relfile|some/file.js|])
|
||||
|
||||
it "Throws error if there is no whitespace after import" $ do
|
||||
isLeft (runWaspParser jsImport "importsomething from \"@ext/some/file.js\"")
|
||||
`shouldBe` True
|
||||
|
||||
it "Throws error if import is not from external code" $ do
|
||||
isLeft (runWaspParser jsImport "import something from \"some/file.js\"")
|
||||
`shouldBe` True
|
||||
|
||||
it "For now we don't support single quotes in FROM part (TODO: support them in the future!)" $ do
|
||||
isLeft (runWaspParser jsImport "import something from '@ext/some/file.js'")
|
||||
`shouldBe` True
|
33
waspc/test/Parser/PageTest.hs
Normal file
33
waspc/test/Parser/PageTest.hs
Normal file
@ -0,0 +1,33 @@
|
||||
module Parser.PageTest where
|
||||
|
||||
import Test.Tasty.Hspec
|
||||
|
||||
import Data.Either (isLeft)
|
||||
|
||||
import Parser.Common (runWaspParser)
|
||||
import Parser.Page (page)
|
||||
import qualified Wasp
|
||||
|
||||
spec_parsePage :: Spec
|
||||
spec_parsePage =
|
||||
describe "Parsing page wasp" $ do
|
||||
let parsePage input = runWaspParser page input
|
||||
|
||||
it "When given valid page wasp declaration, returns correct Wasp.Page" $ do
|
||||
let testPageName = "Landing"
|
||||
let testPageRoute = "/someRoute"
|
||||
let testPageContent = "<span/>"
|
||||
parsePage (
|
||||
"page " ++ testPageName ++ " { " ++
|
||||
"route: \"" ++ testPageRoute ++ "\"," ++
|
||||
"content: {=jsx " ++ testPageContent ++ " jsx=}" ++
|
||||
"}")
|
||||
`shouldBe` Right (Wasp.Page
|
||||
{ Wasp.pageName = testPageName
|
||||
, Wasp.pageRoute = testPageRoute
|
||||
, Wasp.pageContent = testPageContent
|
||||
, Wasp.pageStyle = Nothing
|
||||
})
|
||||
|
||||
it "When given page wasp declaration without 'page', should return Left" $ do
|
||||
isLeft (parsePage "Landing { route: someRoute }") `shouldBe` True
|
106
waspc/test/Parser/ParserTest.hs
Normal file
106
waspc/test/Parser/ParserTest.hs
Normal file
@ -0,0 +1,106 @@
|
||||
module Parser.ParserTest where
|
||||
|
||||
import Test.Tasty.Hspec
|
||||
import Data.Either
|
||||
import Path (relfile)
|
||||
|
||||
import Parser
|
||||
import Wasp
|
||||
import qualified Wasp.EntityForm as EF
|
||||
import qualified Wasp.EntityList as EL
|
||||
import qualified Wasp.Style
|
||||
import qualified Wasp.JsCode
|
||||
|
||||
|
||||
spec_parseWasp :: Spec
|
||||
spec_parseWasp =
|
||||
describe "Parsing wasp" $ do
|
||||
it "When given wasp without app, should return Left" $ do
|
||||
isLeft (parseWasp "hoho") `shouldBe` True
|
||||
|
||||
before (readFile "test/Parser/valid.wasp") $ do
|
||||
it "When given a valid wasp source, should return correct\
|
||||
\ Wasp" $ \wasp -> do
|
||||
parseWasp wasp
|
||||
`shouldBe`
|
||||
Right (fromWaspElems
|
||||
[ WaspElementApp $ App
|
||||
{ appName = "test_app"
|
||||
, appTitle = "Hello World!"
|
||||
}
|
||||
, WaspElementPage $ Page
|
||||
{ pageName = "Landing"
|
||||
, pageRoute = "/"
|
||||
-- TODO: This is heavily hardcoded and hard to maintain, we should find
|
||||
-- better way to test this (test a property, not exact text?) Or keep valid.wasp simple?
|
||||
-- Or use manual snapshot file as Matija suggested?
|
||||
, pageContent = "<div>\n\
|
||||
\ My landing page! I have { this.props.taskList.length } tasks.\n\
|
||||
\\n\
|
||||
\ <div>\n\
|
||||
\ <TaskCreateForm\n\
|
||||
\ onCreate={task => this.props.addTask(task)}\n\
|
||||
\ submitButtonLabel={'Create new task'}\n\
|
||||
\ />\n\
|
||||
\ </div>\n\
|
||||
\\n\
|
||||
\ My tasks\n\
|
||||
\ <TaskList />\n\
|
||||
\ </div>"
|
||||
, pageStyle = Just $ Wasp.Style.CssCode "div {\n\
|
||||
\ color: red\n\
|
||||
\ }"
|
||||
}
|
||||
, WaspElementPage $ Page
|
||||
{ pageName = "TestPage"
|
||||
, pageRoute = "/test"
|
||||
, pageContent = "<div>This is a test page!</div>"
|
||||
, pageStyle = Just $ Wasp.Style.ExtCodeCssFile [relfile|test.css|]
|
||||
}
|
||||
, WaspElementEntity $ Entity
|
||||
{ entityName = "Task"
|
||||
, entityFields =
|
||||
[ Wasp.EntityField "description" Wasp.EftString
|
||||
, Wasp.EntityField "isDone" Wasp.EftBoolean
|
||||
]
|
||||
}
|
||||
, WaspElementEntityForm $ EF.EntityForm
|
||||
{ EF._name = "CreateTaskForm"
|
||||
, EF._entityName = "Task"
|
||||
, EF._submit = Just EF.Submit
|
||||
{ EF._onEnter = Just False
|
||||
, EF._submitButton = Just EF.SubmitButton
|
||||
{ EF._submitButtonShow = Just True
|
||||
}
|
||||
}
|
||||
, EF._fields =
|
||||
[ EF.Field
|
||||
{ EF._fieldName = "description"
|
||||
, EF._fieldShow = Just True
|
||||
, EF._fieldDefaultValue = Just $ EF.DefaultValueString "doable task"
|
||||
, EF._fieldLabel = Just Nothing
|
||||
, EF._fieldPlaceholder = Just "What will you do?"
|
||||
}
|
||||
, EF.Field
|
||||
{ EF._fieldName = "isDone"
|
||||
, EF._fieldShow = Just False
|
||||
, EF._fieldDefaultValue = Just $ EF.DefaultValueBool False
|
||||
, EF._fieldLabel = Nothing
|
||||
, EF._fieldPlaceholder = Nothing
|
||||
}
|
||||
]
|
||||
}
|
||||
, WaspElementEntityList $ EL.EntityList
|
||||
{ EL._name = "TaskList"
|
||||
, EL._entityName = "Task"
|
||||
, EL._showHeader = Just False
|
||||
, EL._fields =
|
||||
[ EL.Field
|
||||
{ EL._fieldName = "description"
|
||||
, EL._fieldRender = Just $ Wasp.JsCode.JsCode "task => task.description"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
`setJsImports` [ JsImport "something" [relfile|some/file|] ]
|
||||
)
|
25
waspc/test/Parser/StyleTest.hs
Normal file
25
waspc/test/Parser/StyleTest.hs
Normal file
@ -0,0 +1,25 @@
|
||||
module Parser.StyleTest where
|
||||
|
||||
import Test.Tasty.Hspec
|
||||
|
||||
import Data.Either (isLeft)
|
||||
import Path (relfile)
|
||||
|
||||
import Parser.Common (runWaspParser)
|
||||
import Parser.Style (style)
|
||||
import qualified Wasp.Style
|
||||
|
||||
|
||||
spec_parseStyle :: Spec
|
||||
spec_parseStyle = do
|
||||
it "Parses external code file path correctly" $ do
|
||||
runWaspParser style "\"@ext/some/file.js\""
|
||||
`shouldBe` Right (Wasp.Style.ExtCodeCssFile [relfile|some/file.js|])
|
||||
|
||||
it "Parses css closure correctly" $ do
|
||||
runWaspParser style "{=css Some css code css=}"
|
||||
`shouldBe` Right (Wasp.Style.CssCode "Some css code")
|
||||
|
||||
it "Throws error if path is not external code path." $ do
|
||||
isLeft (runWaspParser style "\"some/file.js\"")
|
||||
`shouldBe` True
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user