mirror of
https://github.com/wasp-lang/wasp.git
synced 2024-11-23 10:14:08 +03:00
Wasp Studio [experimental] (#1483)
This commit is contained in:
parent
88d9534fd3
commit
03cfcdfaaf
@ -21,6 +21,10 @@ export default defineConfig({
|
||||
|
||||
⚠️ Be careful when changing the dev server port, you'll need to update the `WASP_WEB_CLIENT_URL` env var in your `.env.server` file.
|
||||
|
||||
### 🚧 [Experimental Feature] Wasp Studio
|
||||
|
||||
Running `wasp studio` in the root of your project starts Wasp Studio which visualises your application and shows you the relationships between pieces of your app. It is an experimental feature which is not yet fully ready, but we are working on it and will be adding more features to it in the future.
|
||||
|
||||
## 0.11.5
|
||||
|
||||
### 🐞 Bug fixes / 🔧 small improvements
|
||||
|
@ -27,6 +27,7 @@ import Wasp.Cli.Command.Dockerfile (printDockerfile)
|
||||
import Wasp.Cli.Command.Info (info)
|
||||
import Wasp.Cli.Command.Start (start)
|
||||
import qualified Wasp.Cli.Command.Start.Db as Command.Start.Db
|
||||
import Wasp.Cli.Command.Studio (studio)
|
||||
import qualified Wasp.Cli.Command.Telemetry as Telemetry
|
||||
import Wasp.Cli.Command.Test (test)
|
||||
import Wasp.Cli.Command.Uninstall (uninstall)
|
||||
@ -56,6 +57,7 @@ main = withUtf8 . (`E.catch` handleInternalErrors) $ do
|
||||
["deps"] -> Command.Call.Deps
|
||||
["dockerfile"] -> Command.Call.Dockerfile
|
||||
["info"] -> Command.Call.Info
|
||||
["studio"] -> Command.Call.Studio
|
||||
["completion"] -> Command.Call.PrintBashCompletionInstruction
|
||||
["completion:generate"] -> Command.Call.GenerateBashCompletionScript
|
||||
["completion:list"] -> Command.Call.BashCompletionListCommands
|
||||
@ -84,6 +86,7 @@ main = withUtf8 . (`E.catch` handleInternalErrors) $ do
|
||||
Command.Call.Compile -> runCommand compile
|
||||
Command.Call.Db dbArgs -> dbCli dbArgs
|
||||
Command.Call.Version -> printVersion
|
||||
Command.Call.Studio -> runCommand studio
|
||||
Command.Call.Uninstall -> runCommand uninstall
|
||||
Command.Call.Build -> runCommand build
|
||||
Command.Call.Telemetry -> runCommand Telemetry.telemetry
|
||||
|
@ -14,6 +14,7 @@ data Call
|
||||
| Deps
|
||||
| Dockerfile
|
||||
| Info
|
||||
| Studio
|
||||
| PrintBashCompletionInstruction
|
||||
| GenerateBashCompletionScript
|
||||
| BashCompletionListCommands
|
||||
|
193
waspc/cli/src/Wasp/Cli/Command/Studio.hs
Normal file
193
waspc/cli/src/Wasp/Cli/Command/Studio.hs
Normal file
@ -0,0 +1,193 @@
|
||||
module Wasp.Cli.Command.Studio
|
||||
( studio,
|
||||
)
|
||||
where
|
||||
|
||||
import Control.Arrow ()
|
||||
import Control.Monad.Except (throwError)
|
||||
import Control.Monad.IO.Class (liftIO)
|
||||
import Data.Aeson (object, (.=))
|
||||
import Data.Aeson.Encode.Pretty (encodePretty)
|
||||
import qualified Data.ByteString.Lazy as BSL
|
||||
import Data.Maybe (fromMaybe, isJust)
|
||||
import StrongPath (relfile, (</>))
|
||||
import qualified StrongPath as SP
|
||||
import StrongPath.Operations ()
|
||||
import qualified System.Directory as Dir
|
||||
import qualified Wasp.AppSpec as AS
|
||||
import qualified Wasp.AppSpec.Api as AS.Api
|
||||
import qualified Wasp.AppSpec.App as AS.App
|
||||
import qualified Wasp.AppSpec.App.Auth as AS.App.Auth
|
||||
import qualified Wasp.AppSpec.App.Db as AS.App.Db
|
||||
import qualified Wasp.AppSpec.Job as AS.Job
|
||||
import Wasp.AppSpec.Operation (Operation (..))
|
||||
import qualified Wasp.AppSpec.Operation as Operation
|
||||
import qualified Wasp.AppSpec.Page as AS.Page
|
||||
import qualified Wasp.AppSpec.Route as AS.Route
|
||||
import qualified Wasp.AppSpec.Valid as ASV
|
||||
import Wasp.Cli.Command (Command, CommandError (CommandError))
|
||||
import Wasp.Cli.Command.Compile (analyze)
|
||||
import Wasp.Cli.Command.Message (cliSendMessageC)
|
||||
import Wasp.Cli.Command.Require (InWaspProject (InWaspProject), require)
|
||||
import qualified Wasp.Cli.Common as Common
|
||||
import qualified Wasp.Message as Msg
|
||||
import qualified Wasp.Project.Studio
|
||||
|
||||
studio :: Command ()
|
||||
studio = do
|
||||
InWaspProject waspDir <- require
|
||||
|
||||
appSpec <- analyze waspDir
|
||||
let (appName, app) = ASV.getApp appSpec
|
||||
|
||||
let appInfoJson =
|
||||
object
|
||||
[ "pages"
|
||||
.= map
|
||||
( \(name, page) ->
|
||||
object
|
||||
[ "name" .= name,
|
||||
"authRequired" .= AS.Page.authRequired page
|
||||
-- "operations" .= [] -- TODO: Add operations that page uses. Not easy.
|
||||
]
|
||||
)
|
||||
(AS.getPages appSpec),
|
||||
"routes"
|
||||
.= map
|
||||
( \(name, route) ->
|
||||
object
|
||||
[ "name" .= name,
|
||||
"path" .= AS.Route.path route,
|
||||
"toPage"
|
||||
.= object
|
||||
[ "name" .= fst (AS.resolveRef appSpec $ AS.Route.to route)
|
||||
]
|
||||
]
|
||||
)
|
||||
(AS.getRoutes appSpec),
|
||||
"apis"
|
||||
.= map
|
||||
( \(name, api) ->
|
||||
object
|
||||
[ "name" .= name,
|
||||
"httpRoute"
|
||||
.= let (method, path) = AS.Api.httpRoute api
|
||||
in object
|
||||
[ "method" .= show method,
|
||||
"path" .= path
|
||||
],
|
||||
"auth" .= AS.Api.auth api,
|
||||
"entities" .= getLinkedEntitiesData appSpec (AS.Api.entities api)
|
||||
]
|
||||
)
|
||||
(AS.getApis appSpec),
|
||||
"jobs"
|
||||
.= map
|
||||
( \(name, job) ->
|
||||
object
|
||||
[ "name" .= name,
|
||||
"schedule" .= (AS.Job.cron <$> AS.Job.schedule job),
|
||||
"entities" .= getLinkedEntitiesData appSpec (AS.Job.entities job)
|
||||
]
|
||||
)
|
||||
(AS.getJobs appSpec),
|
||||
"operations"
|
||||
.= map
|
||||
( \operation ->
|
||||
object
|
||||
[ "type" .= case operation of
|
||||
_op@(QueryOp _ _) -> "query" :: String
|
||||
_op@(ActionOp _ _) -> "action",
|
||||
"name" .= Operation.getName operation,
|
||||
"entities"
|
||||
.= getLinkedEntitiesData appSpec (Operation.getEntities operation),
|
||||
"auth" .= Operation.getAuth operation
|
||||
]
|
||||
)
|
||||
(AS.getOperations appSpec),
|
||||
"entities"
|
||||
.= map
|
||||
( \(name, _entity) ->
|
||||
object
|
||||
[ "name" .= name
|
||||
]
|
||||
)
|
||||
(AS.getEntities appSpec),
|
||||
"app"
|
||||
.= object
|
||||
[ "name" .= (appName :: String),
|
||||
"auth" .= getAuthInfo appSpec app,
|
||||
"db" .= getDbInfo app
|
||||
]
|
||||
-- TODO: Add CRUDs.
|
||||
]
|
||||
|
||||
let generatedProjectDir =
|
||||
waspDir </> Common.dotWaspDirInWaspProjectDir
|
||||
</> Common.generatedCodeDirInDotWaspDir
|
||||
|
||||
let waspStudioDataJsonFilePath = generatedProjectDir </> [relfile|.wasp-studio-data.json|]
|
||||
liftIO $ do
|
||||
Dir.createDirectoryIfMissing True $ SP.fromAbsDir $ SP.parent waspStudioDataJsonFilePath
|
||||
BSL.writeFile (SP.fromAbsFile waspStudioDataJsonFilePath) (encodePretty appInfoJson)
|
||||
|
||||
cliSendMessageC . Msg.Info $
|
||||
unlines
|
||||
[ "✨ Starting Wasp Studio ✨",
|
||||
"",
|
||||
"➜ Open in your browser: http://localhost:4000",
|
||||
"",
|
||||
"Wasp Studio visualises your app and lets you understand how different parts of your app are connected."
|
||||
]
|
||||
|
||||
result <- liftIO $ do
|
||||
Wasp.Project.Studio.startStudio $ SP.toFilePath waspStudioDataJsonFilePath
|
||||
|
||||
either (throwError . CommandError "Studio command failed") return result
|
||||
where
|
||||
getLinkedEntitiesData spec entityRefs =
|
||||
map
|
||||
( \(entityName, _entity) ->
|
||||
object ["name" .= entityName]
|
||||
)
|
||||
$ resolveEntities spec entityRefs
|
||||
|
||||
resolveEntities spec entityRefs =
|
||||
AS.resolveRef spec <$> fromMaybe [] entityRefs
|
||||
|
||||
getDbInfo app = do
|
||||
db <- AS.App.db app
|
||||
return $
|
||||
object
|
||||
[ "system" .= (show <$> AS.App.Db.system db)
|
||||
]
|
||||
|
||||
getAuthInfo spec app = do
|
||||
auth <- AS.App.auth app
|
||||
return $
|
||||
object
|
||||
[ "userEntity"
|
||||
.= object
|
||||
[ "name" .= fst (AS.resolveRef spec $ AS.App.Auth.userEntity auth)
|
||||
],
|
||||
"methods"
|
||||
.= let methods = AS.App.Auth.methods auth
|
||||
in -- TODO: Make this type safe, so it gives compile time error/warning if
|
||||
-- new field is added to AuthMethods and we haven't covered it here.
|
||||
-- Best to use TH here to generate this object from AuthMethods?
|
||||
concat
|
||||
[ [ "usernameAndPassword"
|
||||
| isJust $ AS.App.Auth.usernameAndPassword methods
|
||||
],
|
||||
[ "google"
|
||||
| isJust $ AS.App.Auth.google methods
|
||||
],
|
||||
[ "gitHub"
|
||||
| isJust $ AS.App.Auth.gitHub methods
|
||||
],
|
||||
[ "email"
|
||||
| isJust $ AS.App.Auth.email methods
|
||||
]
|
||||
] ::
|
||||
[String]
|
||||
]
|
@ -5,7 +5,7 @@ app pgVectorExample {
|
||||
title: "PG Vector Example",
|
||||
dependencies: [
|
||||
("openai", "^4.5.0"),
|
||||
("react-hook-form", "^7.43.1"),
|
||||
("react-hook-form", "^7.45.4"),
|
||||
("@nextui-org/react", "^2.1.10"),
|
||||
("framer-motion", "^10.16.4"),
|
||||
("pgvector", "0.1.5"),
|
||||
|
4
waspc/packages/studio/.gitignore
vendored
Normal file
4
waspc/packages/studio/.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
node_modules/
|
||||
dist/
|
||||
.env
|
||||
public/
|
44
waspc/packages/studio/README.md
Normal file
44
waspc/packages/studio/README.md
Normal file
@ -0,0 +1,44 @@
|
||||
# Wasp Studio
|
||||
|
||||
Wasp Studio has two components:
|
||||
- the server
|
||||
- the client
|
||||
|
||||
To develop the studio, you need to run both of them. First, run the server, then the client.
|
||||
|
||||
### Server
|
||||
|
||||
Install dependencies:
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
Running the server for some data file:
|
||||
|
||||
```bash
|
||||
npm run dev -- -- -d <path_to_data_file>
|
||||
```
|
||||
|
||||
For example, running the server with the data file from the `examples` directory:
|
||||
|
||||
```bash
|
||||
npm run dev -- -- -d ../../examples/crud-testing/.wasp/out/.wasp-studio-data.json
|
||||
```
|
||||
|
||||
### Client
|
||||
|
||||
Install dependencies:
|
||||
|
||||
```bash
|
||||
cd client
|
||||
npm install
|
||||
```
|
||||
|
||||
Running the client:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Then open `http://localhost:5173` in your browser.
|
18
waspc/packages/studio/client/.eslintrc.cjs
Normal file
18
waspc/packages/studio/client/.eslintrc.cjs
Normal file
@ -0,0 +1,18 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: { browser: true, es2020: true },
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:react-hooks/recommended',
|
||||
],
|
||||
ignorePatterns: ['dist', '.eslintrc.cjs'],
|
||||
parser: '@typescript-eslint/parser',
|
||||
plugins: ['react-refresh'],
|
||||
rules: {
|
||||
'react-refresh/only-export-components': [
|
||||
'warn',
|
||||
{ allowConstantExport: true },
|
||||
],
|
||||
},
|
||||
}
|
24
waspc/packages/studio/client/.gitignore
vendored
Normal file
24
waspc/packages/studio/client/.gitignore
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
17
waspc/packages/studio/client/README.md
Normal file
17
waspc/packages/studio/client/README.md
Normal file
@ -0,0 +1,17 @@
|
||||
# Wasp Studio Client
|
||||
|
||||
It's a React + Vite app that connects via WebSocket to the Wasp Studio Server.
|
||||
|
||||
### Development
|
||||
|
||||
Install deps:
|
||||
|
||||
```
|
||||
npm install
|
||||
```
|
||||
|
||||
Make sure you are running the Wasp Studio Server locally. And then run:
|
||||
|
||||
```
|
||||
npm run dev
|
||||
```
|
13
waspc/packages/studio/client/index.html
Normal file
13
waspc/packages/studio/client/index.html
Normal file
@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Wasp Studio</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
6142
waspc/packages/studio/client/package-lock.json
generated
Normal file
6142
waspc/packages/studio/client/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
37
waspc/packages/studio/client/package.json
Normal file
37
waspc/packages/studio/client/package.json
Normal file
@ -0,0 +1,37 @@
|
||||
{
|
||||
"name": "wasp-studio-client",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"copy": "npm run build && cp -r ./dist/* ../public",
|
||||
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nextui-org/react": "^2.1.12",
|
||||
"elkjs": "^0.8.2",
|
||||
"framer-motion": "^10.16.4",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"reactflow": "^11.8.3",
|
||||
"socket.io-client": "^4.7.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.2.15",
|
||||
"@types/react-dom": "^18.2.7",
|
||||
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
||||
"@typescript-eslint/parser": "^6.0.0",
|
||||
"@vitejs/plugin-react-swc": "^3.3.2",
|
||||
"autoprefixer": "^10.4.15",
|
||||
"eslint": "^8.45.0",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.3",
|
||||
"postcss": "^8.4.29",
|
||||
"tailwindcss": "^3.3.3",
|
||||
"typescript": "^5.0.2",
|
||||
"vite": "^4.4.5"
|
||||
}
|
||||
}
|
6
waspc/packages/studio/client/postcss.config.js
Normal file
6
waspc/packages/studio/client/postcss.config.js
Normal file
@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
58
waspc/packages/studio/client/src/App.tsx
Normal file
58
waspc/packages/studio/client/src/App.tsx
Normal file
@ -0,0 +1,58 @@
|
||||
import { lazy, Suspense } from "react";
|
||||
import {
|
||||
Navbar,
|
||||
NavbarBrand,
|
||||
NavbarContent,
|
||||
NavbarItem,
|
||||
} from "@nextui-org/react";
|
||||
import "reactflow/dist/style.css";
|
||||
|
||||
import { useSocket } from "./socket";
|
||||
import { Logo } from "./Logo";
|
||||
|
||||
const Flow = lazy(() => import("./Flow"));
|
||||
|
||||
export default function App() {
|
||||
const { data, isConnected } = useSocket();
|
||||
|
||||
return (
|
||||
<div className="h-full">
|
||||
<Navbar position="static">
|
||||
<NavbarBrand>
|
||||
<Logo className="w-8 h-8" />
|
||||
<p className="font-bold text-inherit ml-4">{data?.app.name}</p>
|
||||
</NavbarBrand>
|
||||
<NavbarContent justify="end">
|
||||
<NavbarItem>
|
||||
<div className="text-sm p-2 w-35">
|
||||
{isConnected ? (
|
||||
/* Green dot */ <span className="flex items-center">
|
||||
Connected
|
||||
<span className="w-2 h-2 bg-green-500 rounded-full inline-block ml-2"></span>
|
||||
</span>
|
||||
) : (
|
||||
/* Red dot */ <span className="flex items-center">
|
||||
Connecting
|
||||
<span className="w-2 h-2 bg-yellow-500 rounded-full inline-block ml-2"></span>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</NavbarItem>
|
||||
</NavbarContent>
|
||||
</Navbar>
|
||||
<div className="flow-container">
|
||||
<Suspense fallback={<Loading />}>
|
||||
{data ? <Flow data={data} /> : <Loading />}
|
||||
</Suspense>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Loading() {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<p className="text-2xl text-gray-500">Loading...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
316
waspc/packages/studio/client/src/Flow.tsx
Normal file
316
waspc/packages/studio/client/src/Flow.tsx
Normal file
@ -0,0 +1,316 @@
|
||||
import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from "react";
|
||||
import { Data } from "./types";
|
||||
import ReactFlow, {
|
||||
Background,
|
||||
Node,
|
||||
Edge,
|
||||
useEdgesState,
|
||||
useNodesState,
|
||||
useReactFlow,
|
||||
} from "reactflow";
|
||||
|
||||
import ELK, { type ElkNode } from "elkjs/lib/elk.bundled.js";
|
||||
|
||||
import { PageNode } from "./graph/Page";
|
||||
import { EntityNode } from "./graph/Entity";
|
||||
import { ActionNode, QueryNode } from "./graph/Operation";
|
||||
import {
|
||||
createActionNode,
|
||||
createApiNode,
|
||||
createAppNode,
|
||||
createEdge,
|
||||
createEntityNode,
|
||||
createJobNode,
|
||||
createPageNode,
|
||||
createQueryNode,
|
||||
createRouteNode,
|
||||
} from "./graph/factories";
|
||||
import { AppNode } from "./graph/App";
|
||||
import { RouteNode } from "./graph/Route";
|
||||
import { ApiNode } from "./graph/Api";
|
||||
import { JobNode } from "./graph/Job";
|
||||
|
||||
const elk = new ELK();
|
||||
|
||||
const getLayoutedElements = (nodes: Node[], edges: Edge[]) => {
|
||||
const graph = {
|
||||
id: "root",
|
||||
// Elk has a *huge* amount of options to configure. To see everything you can
|
||||
// tweak check out:
|
||||
//
|
||||
// - https://www.eclipse.org/elk/reference/algorithms.html
|
||||
// - https://www.eclipse.org/elk/reference/options.html
|
||||
layoutOptions: {
|
||||
// Alternative layout:
|
||||
// "elk.spacing.nodeNode": "30.0",
|
||||
// "elk.algorithm": "elk.layered",
|
||||
// "elk.layered.spacing.nodeNodeBetweenLayers": "100.0",
|
||||
// "elk.layered.thoroughness": "7",
|
||||
// "elk.direction": "RIGHT",
|
||||
// "elk.edgeRouting": "POLYLINE",
|
||||
// "elk.aspectRatio": "1.0f",
|
||||
"elk.algorithm": "layered",
|
||||
"elk.direction": "RIGHT",
|
||||
"elk.edgeRouting": "POLYLINE",
|
||||
// "elk.hierarchyHandling": "INCLUDE_CHILDREN",
|
||||
"elk.layered.crossingMinimization.semiInteractive": true,
|
||||
},
|
||||
children: nodes.map((node: Node) => ({
|
||||
...node,
|
||||
width: getNodeWidth(node),
|
||||
height: getNodeHeight(node),
|
||||
})),
|
||||
edges: edges,
|
||||
};
|
||||
|
||||
return (
|
||||
elk
|
||||
// Hack
|
||||
.layout(graph as unknown as ElkNode)
|
||||
.then((layoutedGraph) => {
|
||||
if (!layoutedGraph.children) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
nodes: layoutedGraph.children.map((node) => ({
|
||||
...node,
|
||||
position: { x: node.x, y: node.y },
|
||||
})),
|
||||
|
||||
edges: layoutedGraph.edges,
|
||||
};
|
||||
})
|
||||
.catch(console.error)
|
||||
);
|
||||
};
|
||||
|
||||
export default function Flow({ data }: { data: Data }) {
|
||||
// NOTE: This is not used. But it might be useful in the future.
|
||||
const [selectedNode] = useState<Node | null>(null);
|
||||
|
||||
const [nodes, setNodes] = useNodesState([]);
|
||||
const [edges, setEdges] = useEdgesState([]);
|
||||
const { fitView } = useReactFlow();
|
||||
const nodeTypes = useMemo(
|
||||
() => ({
|
||||
pageNode: PageNode,
|
||||
entityNode: EntityNode,
|
||||
queryNode: QueryNode,
|
||||
actionNode: ActionNode,
|
||||
appNode: AppNode,
|
||||
routeNode: RouteNode,
|
||||
apiNode: ApiNode,
|
||||
jobNode: JobNode,
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
const onLayout = useCallback(() => {
|
||||
const initialNodes: Node[] = [
|
||||
// ASSUMPTION: The names are of everything is unique.
|
||||
createAppNode(
|
||||
generateId(data.app.name, "app"),
|
||||
data.app.name,
|
||||
data.app,
|
||||
selectedNode
|
||||
),
|
||||
...data.pages.map((page) =>
|
||||
createPageNode(
|
||||
generateId(page.name, "page"),
|
||||
page.name,
|
||||
page,
|
||||
selectedNode
|
||||
)
|
||||
),
|
||||
...data.operations
|
||||
.filter((operation) => operation.type === "query")
|
||||
.map((query) =>
|
||||
createQueryNode(
|
||||
generateId(query.name, "query"),
|
||||
query.name,
|
||||
query,
|
||||
selectedNode
|
||||
)
|
||||
),
|
||||
...data.operations
|
||||
.filter((operation) => operation.type === "action")
|
||||
.map((action) =>
|
||||
createActionNode(
|
||||
generateId(action.name, "action"),
|
||||
action.name,
|
||||
action,
|
||||
selectedNode
|
||||
)
|
||||
),
|
||||
...data.entities.map((entity) =>
|
||||
createEntityNode(
|
||||
generateId(entity.name, "entity"),
|
||||
entity.name,
|
||||
entity.name === data.app.auth?.userEntity.name,
|
||||
entity,
|
||||
selectedNode
|
||||
)
|
||||
),
|
||||
...data.routes.map((route) =>
|
||||
createRouteNode(
|
||||
generateId(route.path, "route"),
|
||||
route.path,
|
||||
route,
|
||||
selectedNode
|
||||
)
|
||||
),
|
||||
...data.apis.map((api) =>
|
||||
createApiNode(generateId(api.name, "api"), api.name, api, selectedNode)
|
||||
),
|
||||
...data.jobs.map((job) =>
|
||||
createJobNode(generateId(job.name, "job"), job.name, job, selectedNode)
|
||||
),
|
||||
];
|
||||
|
||||
const initialEdges: Edge[] = [
|
||||
...data.entities.map((entity) =>
|
||||
createEdge(
|
||||
generateId(entity.name, "entity"),
|
||||
generateId(data.app.name, "app"),
|
||||
selectedNode
|
||||
)
|
||||
),
|
||||
...data.routes.map((route) =>
|
||||
createEdge(
|
||||
generateId(route.path, "route"),
|
||||
generateId(route.toPage.name, "page"),
|
||||
selectedNode
|
||||
)
|
||||
),
|
||||
...data.operations.flatMap((operation) =>
|
||||
operation.entities.map((entity) =>
|
||||
// ASSUMPTION: operation.type is either "query" or "action"
|
||||
createEdge(
|
||||
generateId(operation.name, operation.type),
|
||||
generateId(entity.name, "entity"),
|
||||
selectedNode
|
||||
)
|
||||
)
|
||||
),
|
||||
...data.apis.flatMap((api) =>
|
||||
api.entities.map((entity) =>
|
||||
createEdge(
|
||||
generateId(api.name, "api"),
|
||||
generateId(entity.name, "entity"),
|
||||
selectedNode
|
||||
)
|
||||
)
|
||||
),
|
||||
...data.jobs.flatMap((job) =>
|
||||
job.entities.map((entity) =>
|
||||
createEdge(
|
||||
generateId(job.name, "job"),
|
||||
generateId(entity.name, "entity"),
|
||||
selectedNode
|
||||
)
|
||||
)
|
||||
),
|
||||
...data.routes.map((route) =>
|
||||
createEdge(
|
||||
generateId(data.app.name, "app"),
|
||||
generateId(route.path, "route"),
|
||||
selectedNode
|
||||
)
|
||||
),
|
||||
];
|
||||
|
||||
getLayoutedElements(initialNodes, initialEdges).then((result) => {
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
const { nodes: layoutedNodes, edges: layoutedEdges } = result;
|
||||
if (!layoutedNodes || !layoutedEdges) {
|
||||
return;
|
||||
}
|
||||
// Hack
|
||||
setNodes(layoutedNodes as Node[]);
|
||||
// Hack
|
||||
setEdges(layoutedEdges as unknown as Edge[]);
|
||||
|
||||
window.requestAnimationFrame(() => fitView());
|
||||
});
|
||||
}, [data, setNodes, setEdges, fitView, selectedNode]);
|
||||
|
||||
// Calculate the initial layout on mount.
|
||||
useLayoutEffect(() => {
|
||||
onLayout();
|
||||
}, [onLayout]);
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
fitView();
|
||||
}, 100);
|
||||
}, [fitView, selectedNode]);
|
||||
|
||||
useEffect(() => {
|
||||
fitView();
|
||||
}, [fitView]);
|
||||
|
||||
return (
|
||||
<div style={{ height: "100%" }}>
|
||||
<ReactFlow nodes={nodes} edges={edges} fitView nodeTypes={nodeTypes}>
|
||||
<Background
|
||||
style={{
|
||||
backgroundColor: `hsl(var(--nextui-background)`,
|
||||
}}
|
||||
color={`#444`}
|
||||
/>
|
||||
</ReactFlow>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function getNodeHeight(node: Node) {
|
||||
if (node.type === "apiNode") {
|
||||
return 100;
|
||||
}
|
||||
if (node.type === "jobNode" && node.data.schedule) {
|
||||
return 100;
|
||||
}
|
||||
if (node.type === "appNode") {
|
||||
const authMethods = node.data.auth?.methods ?? [];
|
||||
return 100 + authMethods.length * 50;
|
||||
}
|
||||
return 50;
|
||||
}
|
||||
|
||||
function getNodeWidth(node: Node) {
|
||||
const textCandidates = [
|
||||
node.data?.label,
|
||||
node.data?.name,
|
||||
node.data?.path,
|
||||
node.data?.schedule,
|
||||
// Auth methods
|
||||
...getAuthMethods(node),
|
||||
]
|
||||
.filter(Boolean)
|
||||
.map((text) => text.length);
|
||||
|
||||
const longestText = Math.max(...textCandidates);
|
||||
const width = Math.max(150, longestText * 10 + 40);
|
||||
return width;
|
||||
}
|
||||
|
||||
function generateId(name: string, type: string): string {
|
||||
return `${type}:${name}`;
|
||||
}
|
||||
|
||||
function getAuthMethods(node: Node) {
|
||||
if (node.type !== "appNode") {
|
||||
return [];
|
||||
}
|
||||
return (
|
||||
node.data?.auth?.methods.map((method: string) => `Auth: ${method}`) ?? []
|
||||
);
|
||||
}
|
52
waspc/packages/studio/client/src/Logo.tsx
Normal file
52
waspc/packages/studio/client/src/Logo.tsx
Normal file
@ -0,0 +1,52 @@
|
||||
import { SVGProps } from "react";
|
||||
export function Logo(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 161 161"
|
||||
width="1em"
|
||||
height="1em"
|
||||
{...props}
|
||||
>
|
||||
<defs>
|
||||
<style>{".cls-2{fill-rule:evenodd}"}</style>
|
||||
</defs>
|
||||
<g>
|
||||
<g>
|
||||
<circle
|
||||
cx={80.5}
|
||||
cy={80.5}
|
||||
r={79}
|
||||
style={{
|
||||
fill: "#f5cc05",
|
||||
}}
|
||||
/>
|
||||
<g>
|
||||
<g>
|
||||
<path
|
||||
d="M88.67 114.33h2.91q6 0 7.87-1.89c1.22-1.25 1.83-3.9 1.83-7.93V93.89c0-4.46.65-7.7 1.93-9.73s3.51-3.43 6.67-4.2q-4.69-1.08-6.65-4.12c-1.3-2-2-5.28-2-9.77V55.44q0-6-1.83-7.93t-7.87-1.88h-2.86V39.5h2.65q10.65 0 14.24 3.15t3.59 12.62v10.29c0 4.28.77 7.24 2.29 8.87s4.3 2.44 8.32 2.44h2.74V83h-2.74q-6 0-8.32 2.49c-1.52 1.65-2.29 4.64-2.29 9v10.25q0 9.47-3.59 12.64t-14.24 3.12h-2.65Z"
|
||||
className="cls-2"
|
||||
/>
|
||||
<path
|
||||
d="M88.67 114.33h2.91q6 0 7.87-1.89c1.22-1.25 1.83-3.9 1.83-7.93V93.89c0-4.46.65-7.7 1.93-9.73s3.51-3.43 6.67-4.2q-4.69-1.08-6.65-4.12c-1.3-2-2-5.28-2-9.77V55.44q0-6-1.83-7.93t-7.87-1.88h-2.86V39.5h2.65q10.65 0 14.24 3.15t3.59 12.62v10.29c0 4.28.77 7.24 2.29 8.87s4.3 2.44 8.32 2.44h2.74V83h-2.74q-6 0-8.32 2.49c-1.52 1.65-2.29 4.64-2.29 9v10.25q0 9.47-3.59 12.64t-14.24 3.12h-2.65Z"
|
||||
className="cls-2"
|
||||
/>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path
|
||||
d="M38.5 85.15h37.33v7.58H38.5Zm0-17.88h37.33v7.49H38.5Z"
|
||||
className="cls-2"
|
||||
/>
|
||||
<path
|
||||
d="M38.5 85.15h37.33v7.58H38.5Zm0-17.88h37.33v7.49H38.5Z"
|
||||
className="cls-2"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
}
|
45
waspc/packages/studio/client/src/graph/Api.tsx
Normal file
45
waspc/packages/studio/client/src/graph/Api.tsx
Normal file
@ -0,0 +1,45 @@
|
||||
import { Handle, NodeProps, Position } from "reactflow";
|
||||
|
||||
export const ApiNode = ({
|
||||
data,
|
||||
sourcePosition = Position.Right,
|
||||
isConnectable,
|
||||
}: NodeProps) => (
|
||||
<div
|
||||
className={`
|
||||
py-3 px-6 rounded bg-slate-900 text-white text-center
|
||||
`}
|
||||
>
|
||||
<Handle
|
||||
type="source"
|
||||
position={sourcePosition}
|
||||
isConnectable={isConnectable}
|
||||
/>
|
||||
<div className="text-xs bg-slate-300 text-slate-900 rounded px-1 absolute -top-1 left-1/2 -translate-x-1/2 flex items-center">
|
||||
<span className="mr-1">API</span>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={1.5}
|
||||
stroke="currentColor"
|
||||
className="w-3 h-3"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M14.25 9.75L16.5 12l-2.25 2.25m-4.5 0L7.5 12l2.25-2.25M6 20.25h12A2.25 2.25 0 0020.25 18V6A2.25 2.25 0 0018 3.75H6A2.25 2.25 0 003.75 6v12A2.25 2.25 0 006 20.25z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="font-bold">{data?.label}</div>
|
||||
<div className="flex justify-center items-center mt-2">
|
||||
<div className="text-xs bg-foreground text-background rounded px-1">
|
||||
<span>
|
||||
<strong className="font-bold">{data.httpRoute.method}</strong>{" "}
|
||||
{data.httpRoute.path}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
53
waspc/packages/studio/client/src/graph/App.tsx
Normal file
53
waspc/packages/studio/client/src/graph/App.tsx
Normal file
@ -0,0 +1,53 @@
|
||||
import { Handle, NodeProps, Position } from "reactflow";
|
||||
|
||||
export const AppNode = ({
|
||||
data,
|
||||
isConnectable,
|
||||
targetPosition = Position.Left,
|
||||
sourcePosition = Position.Right,
|
||||
}: NodeProps) => (
|
||||
<div className="py-3 px-6 rounded bg-cyan-900 text-white text-center">
|
||||
<Handle
|
||||
type="target"
|
||||
position={targetPosition}
|
||||
isConnectable={isConnectable}
|
||||
/>
|
||||
<div className="text-xs bg-cyan-300 text-cyan-900 rounded px-1 absolute -top-1 left-1/2 -translate-x-1/2 flex items-center">
|
||||
<span className="mr-1">App</span>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={1.5}
|
||||
stroke="currentColor"
|
||||
className="w-3 h-3"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M8.25 3v1.5M4.5 8.25H3m18 0h-1.5M4.5 12H3m18 0h-1.5m-15 3.75H3m18 0h-1.5M8.25 19.5V21M12 3v1.5m0 15V21m3.75-18v1.5m0 15V21m-9-1.5h10.5a2.25 2.25 0 002.25-2.25V6.75a2.25 2.25 0 00-2.25-2.25H6.75A2.25 2.25 0 004.5 6.75v10.5a2.25 2.25 0 002.25 2.25zm.75-12h9v9h-9v-9z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="font-bold">{data?.label}</div>
|
||||
<Handle
|
||||
type="source"
|
||||
position={sourcePosition}
|
||||
isConnectable={isConnectable}
|
||||
/>
|
||||
<div className="flex justify-center flex-col items-center mt-2 gap-2">
|
||||
<div className="text-xs bg-foreground text-background rounded px-1">
|
||||
<span>{data.db?.system || "SQLite"}</span>
|
||||
</div>
|
||||
{data.auth &&
|
||||
data.auth.methods.map((method: string) => (
|
||||
<div
|
||||
className="text-xs bg-foreground text-background rounded px-1"
|
||||
key={method}
|
||||
>
|
||||
<span>Auth: {method}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
48
waspc/packages/studio/client/src/graph/Entity.tsx
Normal file
48
waspc/packages/studio/client/src/graph/Entity.tsx
Normal file
@ -0,0 +1,48 @@
|
||||
import { Handle, NodeProps, Position } from "reactflow";
|
||||
|
||||
export const EntityNode = ({
|
||||
data,
|
||||
isConnectable,
|
||||
targetPosition = Position.Left,
|
||||
sourcePosition = Position.Right,
|
||||
}: NodeProps) => (
|
||||
<div
|
||||
className={`
|
||||
py-3 px-6 rounded bg-yellow-900 text-white text-center
|
||||
${data.isUserEntity ? "border-3 border-yellow-300" : ""}
|
||||
`}
|
||||
>
|
||||
<Handle
|
||||
type="source"
|
||||
position={sourcePosition}
|
||||
isConnectable={isConnectable}
|
||||
/>
|
||||
<div
|
||||
className={`
|
||||
text-xs bg-yellow-300 text-yellow-900 rounded px-1 absolute -top-1 left-1/2 -translate-x-1/2 flex items-center
|
||||
`}
|
||||
>
|
||||
<span className="mr-1">Entity</span>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={1.5}
|
||||
stroke="currentColor"
|
||||
className="w-3 h-3"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M15.75 6a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0zM4.501 20.118a7.5 7.5 0 0114.998 0A17.933 17.933 0 0112 21.75c-2.676 0-5.216-.584-7.499-1.632z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="font-bold">{data?.label}</div>
|
||||
<Handle
|
||||
type="target"
|
||||
position={targetPosition}
|
||||
isConnectable={isConnectable}
|
||||
/>
|
||||
</div>
|
||||
);
|
40
waspc/packages/studio/client/src/graph/Job.tsx
Normal file
40
waspc/packages/studio/client/src/graph/Job.tsx
Normal file
@ -0,0 +1,40 @@
|
||||
import { Handle, NodeProps, Position } from "reactflow";
|
||||
|
||||
export const JobNode = ({
|
||||
data,
|
||||
isConnectable,
|
||||
sourcePosition = Position.Right,
|
||||
}: NodeProps) => (
|
||||
<div className="py-3 px-6 rounded bg-violet-900 text-white text-center">
|
||||
<Handle
|
||||
type="source"
|
||||
position={sourcePosition}
|
||||
isConnectable={isConnectable}
|
||||
/>
|
||||
<div className="text-xs bg-violet-300 text-violet-900 rounded px-1 absolute -top-1 left-1/2 -translate-x-1/2 flex items-center">
|
||||
<span className="mr-1">Job</span>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={1.5}
|
||||
stroke="currentColor"
|
||||
className="w-3 h-3"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M19.5 12c0-1.232-.046-2.453-.138-3.662a4.006 4.006 0 00-3.7-3.7 48.678 48.678 0 00-7.324 0 4.006 4.006 0 00-3.7 3.7c-.017.22-.032.441-.046.662M19.5 12l3-3m-3 3l-3-3m-12 3c0 1.232.046 2.453.138 3.662a4.006 4.006 0 003.7 3.7 48.656 48.656 0 007.324 0 4.006 4.006 0 003.7-3.7c.017-.22.032-.441.046-.662M4.5 12l3 3m-3-3l-3 3"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="font-bold">{data?.label}</div>
|
||||
{data.schedule && (
|
||||
<div className="flex justify-center items-center mt-2">
|
||||
<div className="text-xs bg-foreground text-background rounded px-1">
|
||||
<span>Schedule: {data.schedule}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
84
waspc/packages/studio/client/src/graph/Operation.tsx
Normal file
84
waspc/packages/studio/client/src/graph/Operation.tsx
Normal file
@ -0,0 +1,84 @@
|
||||
import { Handle, NodeProps, Position } from "reactflow";
|
||||
|
||||
export const QueryNode = (props: NodeProps) => (
|
||||
<OperationNode
|
||||
{...props}
|
||||
label={
|
||||
<div className="flex items-center">
|
||||
<span className="mr-1">Query</span>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={1.5}
|
||||
stroke="currentColor"
|
||||
className="w-3 h-3"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M9.879 7.519c1.171-1.025 3.071-1.025 4.242 0 1.172 1.025 1.172 2.687 0 3.712-.203.179-.43.326-.67.442-.745.361-1.45.999-1.45 1.827v.75M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9 5.25h.008v.008H12v-.008z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
}
|
||||
color="emerald"
|
||||
/>
|
||||
);
|
||||
|
||||
export const ActionNode = (props: NodeProps) => (
|
||||
<OperationNode
|
||||
{...props}
|
||||
label={
|
||||
<div className="flex items-center">
|
||||
<span className="mr-1">Action</span>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={1.5}
|
||||
stroke="currentColor"
|
||||
className="w-3 h-3"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M11.25 4.5l7.5 7.5-7.5 7.5m-6-15l7.5 7.5-7.5 7.5"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
}
|
||||
color="pink"
|
||||
/>
|
||||
);
|
||||
|
||||
export const OperationNode = ({
|
||||
data,
|
||||
isConnectable,
|
||||
targetPosition = Position.Top,
|
||||
sourcePosition = Position.Bottom,
|
||||
label = "Operation",
|
||||
color = "emerald",
|
||||
}: NodeProps & {
|
||||
label?: React.ReactNode;
|
||||
color?: string;
|
||||
}) => (
|
||||
<div className={`py-3 px-6 rounded bg-${color}-900 text-white`}>
|
||||
<Handle
|
||||
type="target"
|
||||
position={targetPosition}
|
||||
isConnectable={isConnectable}
|
||||
/>
|
||||
<div
|
||||
className={`text-xs bg-${color}-300 text-${color}-900 rounded px-1 absolute -top-1 left-1/2 -translate-x-1/2`}
|
||||
>
|
||||
{label}
|
||||
</div>
|
||||
<div className="font-bold">{data?.label}</div>
|
||||
<Handle
|
||||
type="source"
|
||||
position={sourcePosition}
|
||||
isConnectable={isConnectable}
|
||||
/>
|
||||
</div>
|
||||
);
|
40
waspc/packages/studio/client/src/graph/Page.tsx
Normal file
40
waspc/packages/studio/client/src/graph/Page.tsx
Normal file
@ -0,0 +1,40 @@
|
||||
import { Handle, NodeProps, Position } from "reactflow";
|
||||
|
||||
export const PageNode = ({
|
||||
data,
|
||||
isConnectable,
|
||||
targetPosition = Position.Right,
|
||||
}: NodeProps) => (
|
||||
<div className="py-3 px-6 rounded bg-sky-900 text-white">
|
||||
<Handle
|
||||
type="target"
|
||||
position={targetPosition}
|
||||
isConnectable={isConnectable}
|
||||
/>
|
||||
<div className="text-xs bg-sky-300 text-sky-900 rounded px-1 absolute -top-1 left-1/2 -translate-x-1/2 flex items-center">
|
||||
<span className="mr-1">Page</span>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={1.5}
|
||||
stroke="currentColor"
|
||||
className="w-3 h-3"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M12 6.042A8.967 8.967 0 006 3.75c-1.052 0-2.062.18-3 .512v14.25A8.987 8.987 0 016 18c2.305 0 4.408.867 6 2.292m0-14.25a8.966 8.966 0 016-2.292c1.052 0 2.062.18 3 .512v14.25A8.987 8.987 0 0018 18a8.967 8.967 0 00-6 2.292m0-14.25v14.25"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="font-bold">
|
||||
{data?.label}
|
||||
{data.authRequired ? (
|
||||
<span className="ml-1" role="img" aria-label="Auth Required">
|
||||
🔒
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
39
waspc/packages/studio/client/src/graph/Route.tsx
Normal file
39
waspc/packages/studio/client/src/graph/Route.tsx
Normal file
@ -0,0 +1,39 @@
|
||||
import { Handle, NodeProps, Position } from "reactflow";
|
||||
|
||||
export const RouteNode = ({
|
||||
data,
|
||||
isConnectable,
|
||||
sourcePosition = Position.Left,
|
||||
targetPosition = Position.Right,
|
||||
}: NodeProps) => (
|
||||
<div className="py-3 px-6 rounded bg-rose-900 text-white text-center">
|
||||
<Handle
|
||||
type="target"
|
||||
position={targetPosition}
|
||||
isConnectable={isConnectable}
|
||||
/>
|
||||
<div className="text-xs bg-rose-300 text-rose-900 rounded px-1 absolute -top-1 left-1/2 -translate-x-1/2 flex items-center">
|
||||
<span className="mr-1">Route</span>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={1.5}
|
||||
stroke="currentColor"
|
||||
className="w-3 h-3"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M15.042 21.672L13.684 16.6m0 0l-2.51 2.225.569-9.47 5.227 7.917-3.286-.672zM12 2.25V4.5m5.834.166l-1.591 1.591M20.25 10.5H18M7.757 14.743l-1.59 1.59M6 10.5H3.75m4.007-4.243l-1.59-1.59"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="font-bold">{data?.label}</div>
|
||||
<Handle
|
||||
type="source"
|
||||
position={sourcePosition}
|
||||
isConnectable={isConnectable}
|
||||
/>
|
||||
</div>
|
||||
);
|
157
waspc/packages/studio/client/src/graph/factories.ts
Normal file
157
waspc/packages/studio/client/src/graph/factories.ts
Normal file
@ -0,0 +1,157 @@
|
||||
import { Position, Node, Edge } from "reactflow";
|
||||
|
||||
let id = 1;
|
||||
|
||||
export function generateId() {
|
||||
return `${id++}`;
|
||||
}
|
||||
|
||||
type AnyData = {
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
export function createPageNode(
|
||||
id: string,
|
||||
name: string,
|
||||
data: AnyData,
|
||||
selectedNode: Node | null
|
||||
) {
|
||||
return {
|
||||
id,
|
||||
type: "pageNode",
|
||||
data: { label: name, ...data },
|
||||
position: { x: 0, y: 0 },
|
||||
targetPosition: Position.Left,
|
||||
selected: selectedNode?.id === id,
|
||||
} satisfies Node;
|
||||
}
|
||||
|
||||
export function createActionNode(
|
||||
id: string,
|
||||
name: string,
|
||||
data: AnyData,
|
||||
selectedNode: Node | null
|
||||
) {
|
||||
return {
|
||||
id,
|
||||
data: { label: name, ...data },
|
||||
position: { x: 0, y: 0 },
|
||||
sourcePosition: Position.Right,
|
||||
type: "actionNode",
|
||||
selected: selectedNode?.id === id,
|
||||
} satisfies Node;
|
||||
}
|
||||
|
||||
export function createQueryNode(
|
||||
id: string,
|
||||
name: string,
|
||||
data: AnyData,
|
||||
selectedNode: Node | null
|
||||
) {
|
||||
return {
|
||||
id,
|
||||
data: { label: name, ...data },
|
||||
position: { x: 0, y: 0 },
|
||||
sourcePosition: Position.Right,
|
||||
type: "queryNode",
|
||||
selected: selectedNode?.id === id,
|
||||
} satisfies Node;
|
||||
}
|
||||
|
||||
export function createRouteNode(
|
||||
id: string,
|
||||
name: string,
|
||||
data: AnyData,
|
||||
selectedNode: Node | null
|
||||
) {
|
||||
return {
|
||||
id,
|
||||
data: { label: name, ...data },
|
||||
position: { x: 0, y: 0 },
|
||||
sourcePosition: Position.Right,
|
||||
targetPosition: Position.Left,
|
||||
type: "routeNode",
|
||||
selected: selectedNode?.id === id,
|
||||
} satisfies Node;
|
||||
}
|
||||
|
||||
export function createEntityNode(
|
||||
id: string,
|
||||
name: string,
|
||||
isUserEntity: boolean,
|
||||
data: AnyData,
|
||||
selectedNode: Node | null
|
||||
) {
|
||||
return {
|
||||
id,
|
||||
data: { label: name, isUserEntity, ...data },
|
||||
position: { x: 0, y: 0 },
|
||||
type: "entityNode",
|
||||
targetPosition: Position.Left,
|
||||
sourcePosition: Position.Right,
|
||||
selected: selectedNode?.id === id,
|
||||
} satisfies Node;
|
||||
}
|
||||
|
||||
export function createApiNode(
|
||||
id: string,
|
||||
name: string,
|
||||
data: AnyData,
|
||||
selectedNode: Node | null
|
||||
) {
|
||||
return {
|
||||
id,
|
||||
data: { label: name, ...data },
|
||||
position: { x: 0, y: 0 },
|
||||
type: "apiNode",
|
||||
sourcePosition: Position.Right,
|
||||
selected: selectedNode?.id === id,
|
||||
} satisfies Node;
|
||||
}
|
||||
|
||||
export function createJobNode(
|
||||
id: string,
|
||||
name: string,
|
||||
data: AnyData,
|
||||
selectedNode: Node | null
|
||||
) {
|
||||
return {
|
||||
id,
|
||||
data: { label: name, ...data },
|
||||
position: { x: 0, y: 0 },
|
||||
type: "jobNode",
|
||||
sourcePosition: Position.Right,
|
||||
selected: selectedNode?.id === id,
|
||||
} satisfies Node;
|
||||
}
|
||||
|
||||
export function createAppNode(
|
||||
id: string,
|
||||
name: string,
|
||||
data: AnyData,
|
||||
selectedNode: Node | null
|
||||
) {
|
||||
return {
|
||||
id,
|
||||
data: { label: name, ...data },
|
||||
position: { x: 0, y: 0 },
|
||||
type: "appNode",
|
||||
targetPosition: Position.Left,
|
||||
sourcePosition: Position.Right,
|
||||
selected: selectedNode?.id === id,
|
||||
} satisfies Node;
|
||||
}
|
||||
|
||||
export function createEdge(
|
||||
source: string,
|
||||
target: string,
|
||||
selectedNode: Node | null
|
||||
) {
|
||||
return {
|
||||
id: `${source}-${target}`,
|
||||
source,
|
||||
target,
|
||||
animated: true,
|
||||
selected: selectedNode?.id === source || selectedNode?.id === target,
|
||||
} satisfies Edge;
|
||||
}
|
8
waspc/packages/studio/client/src/index.css
Normal file
8
waspc/packages/studio/client/src/index.css
Normal file
@ -0,0 +1,8 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
.flow-container {
|
||||
height: calc(100vh - 4rem);
|
||||
width: 100vw;
|
||||
}
|
18
waspc/packages/studio/client/src/main.tsx
Normal file
18
waspc/packages/studio/client/src/main.tsx
Normal file
@ -0,0 +1,18 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import "./index.css";
|
||||
import App from "./App.tsx";
|
||||
import { NextUIProvider } from "@nextui-org/react";
|
||||
import { ReactFlowProvider } from "reactflow";
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||
<React.StrictMode>
|
||||
<ReactFlowProvider>
|
||||
<NextUIProvider>
|
||||
<div className="dark text-foreground bg-background">
|
||||
<App />
|
||||
</div>
|
||||
</NextUIProvider>
|
||||
</ReactFlowProvider>
|
||||
</React.StrictMode>
|
||||
);
|
44
waspc/packages/studio/client/src/socket.ts
Normal file
44
waspc/packages/studio/client/src/socket.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { io } from "socket.io-client";
|
||||
import { Data } from "./types";
|
||||
|
||||
export const socket = io("http://localhost:4000");
|
||||
|
||||
export function useSocket() {
|
||||
const [isConnected, setIsConnected] = useState(false);
|
||||
|
||||
const [data, setData] = useState<Data | null>(null);
|
||||
|
||||
function onData(data: string) {
|
||||
try {
|
||||
setData(JSON.parse(data) as Data);
|
||||
} catch (e: unknown) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
function onConnect() {
|
||||
setIsConnected(true);
|
||||
}
|
||||
|
||||
function onDisconnect() {
|
||||
setIsConnected(false);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
socket.on("connect", onConnect);
|
||||
socket.on("disconnect", onDisconnect);
|
||||
socket.on("data", onData);
|
||||
return () => {
|
||||
socket.off("data", onData);
|
||||
socket.off("connect", onConnect);
|
||||
socket.off("disconnect", onDisconnect);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return {
|
||||
data,
|
||||
socket,
|
||||
isConnected,
|
||||
};
|
||||
}
|
54
waspc/packages/studio/client/src/types.ts
Normal file
54
waspc/packages/studio/client/src/types.ts
Normal file
@ -0,0 +1,54 @@
|
||||
export type Data = {
|
||||
entities: {
|
||||
name: string;
|
||||
}[];
|
||||
operations: {
|
||||
entities: {
|
||||
name: string;
|
||||
}[];
|
||||
name: string;
|
||||
type: "query" | "action";
|
||||
auth: string;
|
||||
}[];
|
||||
apis: {
|
||||
entities: {
|
||||
name: string;
|
||||
}[];
|
||||
httpRoute: {
|
||||
method: string;
|
||||
path: string;
|
||||
};
|
||||
name: string;
|
||||
auth: string;
|
||||
}[];
|
||||
jobs: {
|
||||
schedule: string;
|
||||
entities: {
|
||||
name: string;
|
||||
}[];
|
||||
name: string;
|
||||
}[];
|
||||
pages: {
|
||||
authRequired: string;
|
||||
name: string;
|
||||
}[];
|
||||
routes: {
|
||||
name: string;
|
||||
toPage: {
|
||||
name: string;
|
||||
};
|
||||
path: string;
|
||||
}[];
|
||||
app: {
|
||||
name: string;
|
||||
auth: {
|
||||
userEntity: {
|
||||
name: string;
|
||||
};
|
||||
methods: string[];
|
||||
};
|
||||
db: {
|
||||
system: string;
|
||||
};
|
||||
};
|
||||
};
|
1
waspc/packages/studio/client/src/vite-env.d.ts
vendored
Normal file
1
waspc/packages/studio/client/src/vite-env.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
23
waspc/packages/studio/client/tailwind.config.js
Normal file
23
waspc/packages/studio/client/tailwind.config.js
Normal file
@ -0,0 +1,23 @@
|
||||
import { nextui } from "@nextui-org/react";
|
||||
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: [
|
||||
"./index.html",
|
||||
"./src/**/*.{js,ts,jsx,tsx}",
|
||||
"./node_modules/@nextui-org/theme/dist/**/*.{js,ts,jsx,tsx}",
|
||||
],
|
||||
safelist: [
|
||||
"bg-pink-900",
|
||||
"bg-pink-300",
|
||||
"text-pink-900",
|
||||
"bg-emerald-900",
|
||||
"bg-emerald-300",
|
||||
"text-emerald-900",
|
||||
],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
darkMode: "class",
|
||||
plugins: [nextui()],
|
||||
};
|
25
waspc/packages/studio/client/tsconfig.json
Normal file
25
waspc/packages/studio/client/tsconfig.json
Normal file
@ -0,0 +1,25 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
10
waspc/packages/studio/client/tsconfig.node.json
Normal file
10
waspc/packages/studio/client/tsconfig.node.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
7
waspc/packages/studio/client/vite.config.ts
Normal file
7
waspc/packages/studio/client/vite.config.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react-swc'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
})
|
7
waspc/packages/studio/nodemon.json
Normal file
7
waspc/packages/studio/nodemon.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"watch": [
|
||||
"./src/**/*.ts",
|
||||
".env"
|
||||
],
|
||||
"exec": "ts-node -r dotenv/config"
|
||||
}
|
1903
waspc/packages/studio/package-lock.json
generated
Normal file
1903
waspc/packages/studio/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
35
waspc/packages/studio/package.json
Normal file
35
waspc/packages/studio/package.json
Normal file
@ -0,0 +1,35 @@
|
||||
{
|
||||
"name": "wasp-studio-server",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"start": "tsc --noEmit && ts-node -r dotenv/config ./src/index.ts",
|
||||
"dev": "nodemon ./src/index.ts",
|
||||
"build:client": "npm --prefix ./client install && npm --prefix ./client run copy",
|
||||
"build": "npm run build:client && rm -rf dist && tsc && cp -r ./public ./dist/public"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@swc/core": "^1.3.52",
|
||||
"@swc/helpers": "^0.5.0",
|
||||
"@tsconfig/node18": "^1.0.1",
|
||||
"@types/node": "^18.15.12",
|
||||
"nodemon": "^2.0.22",
|
||||
"regenerator-runtime": "^0.13.11",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^5.0.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fastify/cors": "^8.3.0",
|
||||
"@fastify/static": "^6.11.2",
|
||||
"commander": "^11.0.0",
|
||||
"dotenv": "^16.0.3",
|
||||
"fastify": "^4.23.2",
|
||||
"fastify-socket.io": "^4.0.0",
|
||||
"socket.io": "^4.7.2"
|
||||
}
|
||||
}
|
0
waspc/packages/studio/public/.gitkeep
Normal file
0
waspc/packages/studio/public/.gitkeep
Normal file
66
waspc/packages/studio/src/index.ts
Normal file
66
waspc/packages/studio/src/index.ts
Normal file
@ -0,0 +1,66 @@
|
||||
import * as fs from "fs";
|
||||
|
||||
import Fastify from "fastify";
|
||||
import FastifySocketIO from "fastify-socket.io";
|
||||
import FastifyStatic from "@fastify/static";
|
||||
import cors from "@fastify/cors";
|
||||
import { Command } from "commander";
|
||||
|
||||
function getUrlFromRelativePathToCwd(path: string) {
|
||||
return new URL(path, `file://${process.cwd()}/`);
|
||||
}
|
||||
|
||||
const program = new Command();
|
||||
program
|
||||
.requiredOption("-d, --data-file <path>", "Path to data file")
|
||||
.parse(process.argv);
|
||||
|
||||
const options = program.opts<{
|
||||
dataFile: string;
|
||||
}>();
|
||||
|
||||
const fastify = Fastify({
|
||||
logger: true,
|
||||
});
|
||||
|
||||
fastify.register(FastifySocketIO, {
|
||||
cors: {
|
||||
origin: "*",
|
||||
},
|
||||
});
|
||||
fastify.register(cors, {
|
||||
origin: true,
|
||||
});
|
||||
fastify.register(FastifyStatic, {
|
||||
root: new URL("./public", import.meta.url).pathname,
|
||||
});
|
||||
|
||||
const pathToDataFile = getUrlFromRelativePathToCwd(options.dataFile);
|
||||
function readFile() {
|
||||
return fs.readFileSync(pathToDataFile, "utf8");
|
||||
}
|
||||
|
||||
let data = readFile();
|
||||
fs.watch(pathToDataFile, () => {
|
||||
data = readFile();
|
||||
fastify.io.emit("data", data);
|
||||
});
|
||||
|
||||
fastify.ready((err) => {
|
||||
if (err) throw err;
|
||||
fastify.io.on("connection", (socket) => {
|
||||
console.log("Client connected");
|
||||
socket.emit("data", data);
|
||||
|
||||
socket.on("disconnect", () => {
|
||||
console.log("Client disconnected");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
try {
|
||||
await fastify.listen({ port: 4000 });
|
||||
} catch (err) {
|
||||
fastify.log.error(err);
|
||||
process.exit(1);
|
||||
}
|
14
waspc/packages/studio/tsconfig.json
Normal file
14
waspc/packages/studio/tsconfig.json
Normal file
@ -0,0 +1,14 @@
|
||||
{
|
||||
"extends": "@tsconfig/node18/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"module": "esnext",
|
||||
"target": "es2017",
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
},
|
||||
"ts-node": {
|
||||
"esm": true,
|
||||
"swc": true,
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
@ -9,6 +9,7 @@ module Wasp.AppSpec
|
||||
refName,
|
||||
getActions,
|
||||
getQueries,
|
||||
getOperations,
|
||||
getApis,
|
||||
getEntities,
|
||||
getPages,
|
||||
@ -39,6 +40,8 @@ import Wasp.AppSpec.Entity (Entity)
|
||||
import Wasp.AppSpec.ExternalCode (SourceExternalCodeDir)
|
||||
import qualified Wasp.AppSpec.ExternalCode as ExternalCode
|
||||
import Wasp.AppSpec.Job (Job)
|
||||
import Wasp.AppSpec.Operation (Operation)
|
||||
import qualified Wasp.AppSpec.Operation as AS.Operation
|
||||
import Wasp.AppSpec.Page (Page)
|
||||
import Wasp.AppSpec.Query (Query)
|
||||
import Wasp.AppSpec.Route (Route)
|
||||
@ -94,6 +97,11 @@ getQueries = getDecls
|
||||
getActions :: AppSpec -> [(String, Action)]
|
||||
getActions = getDecls
|
||||
|
||||
getOperations :: AppSpec -> [Operation]
|
||||
getOperations spec =
|
||||
map (uncurry AS.Operation.QueryOp) (getQueries spec)
|
||||
<> map (uncurry AS.Operation.ActionOp) (getActions spec)
|
||||
|
||||
getApis :: AppSpec -> [(String, Api)]
|
||||
getApis = getDecls
|
||||
|
||||
|
@ -24,6 +24,7 @@ import Wasp.Node.Version (getAndCheckNodeVersion)
|
||||
data Package
|
||||
= DeployPackage
|
||||
| TsInspectPackage
|
||||
| WaspStudioPackage
|
||||
|
||||
data PackagesDir
|
||||
|
||||
@ -37,6 +38,7 @@ packagesDirInDataDir = [reldir|packages|]
|
||||
packageDirInPackagesDir :: Package -> Path' (Rel PackagesDir) (Dir PackageDir)
|
||||
packageDirInPackagesDir DeployPackage = [reldir|deploy|]
|
||||
packageDirInPackagesDir TsInspectPackage = [reldir|ts-inspect|]
|
||||
packageDirInPackagesDir WaspStudioPackage = [reldir|studio|]
|
||||
|
||||
scriptInPackageDir :: Path' (Rel PackageDir) (File PackageScript)
|
||||
scriptInPackageDir = [relfile|dist/index.js|]
|
||||
|
34
waspc/src/Wasp/Project/Studio.hs
Normal file
34
waspc/src/Wasp/Project/Studio.hs
Normal file
@ -0,0 +1,34 @@
|
||||
module Wasp.Project.Studio
|
||||
( startStudio,
|
||||
)
|
||||
where
|
||||
|
||||
import System.Exit (ExitCode (..))
|
||||
import qualified System.Process as P
|
||||
import Wasp.NodePackageFFI (Package (WaspStudioPackage), getPackageProcessOptions)
|
||||
|
||||
startStudio ::
|
||||
-- | Path to the data JSON file.
|
||||
FilePath ->
|
||||
-- | All arguments from the Wasp CLI.
|
||||
IO (Either String ())
|
||||
startStudio pathToDataFile = do
|
||||
let startStudioArgs = ["--data-file", pathToDataFile]
|
||||
|
||||
cp <- getPackageProcessOptions WaspStudioPackage startStudioArgs
|
||||
-- Set up the process so that it:
|
||||
-- - Inherits handles from the waspc process (it will print and read from stdin/out/err)
|
||||
-- - Delegates Ctrl+C: when waspc receives Ctrl+C while this process is running,
|
||||
-- it will properly shut-down the child process.
|
||||
-- See https://hackage.haskell.org/package/process-1.6.17.0/docs/System-Process.html#g:4.
|
||||
let cpInheritHandles =
|
||||
cp
|
||||
{ P.std_in = P.Inherit,
|
||||
P.std_out = P.Inherit,
|
||||
P.std_err = P.Inherit,
|
||||
P.delegate_ctlc = True
|
||||
}
|
||||
exitCode <- P.withCreateProcess cpInheritHandles $ \_ _ _ ph -> P.waitForProcess ph
|
||||
case exitCode of
|
||||
ExitSuccess -> return $ Right ()
|
||||
ExitFailure code -> return $ Left $ "Studio command failed with exit code: " ++ show code
|
@ -62,6 +62,12 @@ data-files:
|
||||
packages/ts-inspect/dist/**/*.js
|
||||
packages/ts-inspect/package.json
|
||||
packages/ts-inspect/package-lock.json
|
||||
packages/studio/dist/**/*.js
|
||||
packages/studio/dist/**/*.html
|
||||
packages/studio/dist/**/*.css
|
||||
packages/studio/dist/**/*.png
|
||||
packages/studio/package.json
|
||||
packages/studio/package-lock.json
|
||||
data-dir: data/
|
||||
|
||||
source-repository head
|
||||
@ -320,6 +326,7 @@ library
|
||||
Wasp.Project.Deployment
|
||||
Wasp.Project.Env
|
||||
Wasp.Project.WebApp
|
||||
Wasp.Project.Studio
|
||||
Wasp.Project.Vite
|
||||
Wasp.NpmDependency
|
||||
Wasp.Psl.Ast.Model
|
||||
@ -415,6 +422,7 @@ library cli-lib
|
||||
, filepath
|
||||
, time
|
||||
, aeson
|
||||
, aeson-pretty
|
||||
, mtl
|
||||
, async
|
||||
, exceptions
|
||||
@ -424,6 +432,7 @@ library cli-lib
|
||||
, optparse-applicative ^>=0.17.0.0
|
||||
, path
|
||||
, path-io
|
||||
, pretty-simple ^>= 4.1.2.0
|
||||
, process
|
||||
, strong-path
|
||||
, text
|
||||
@ -466,6 +475,7 @@ library cli-lib
|
||||
Wasp.Cli.Command.Deploy
|
||||
Wasp.Cli.Command.Dockerfile
|
||||
Wasp.Cli.Command.Info
|
||||
Wasp.Cli.Command.Studio
|
||||
Wasp.Cli.Command.Require
|
||||
Wasp.Cli.Command.Start
|
||||
Wasp.Cli.Command.Start.Db
|
||||
|
Loading…
Reference in New Issue
Block a user