Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Using TS SDK from waspc #2276

Open
wants to merge 37 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
e228263
Successfully get spec from stdout
sodic Sep 6, 2024
664eafe
Successfully read spec from file
sodic Sep 6, 2024
1b75ab9
Clean up code in Analyze.hs
sodic Sep 10, 2024
0c7b901
Move outDir source of truth into Haskell
sodic Sep 10, 2024
b7c4800
Add extra context to Analyze.hs
sodic Sep 11, 2024
68bfe30
Improve TS SDK analysis
sodic Sep 11, 2024
0758747
Put config dir back in .wasp dir
sodic Sep 11, 2024
b7ecab4
Add more comments
sodic Sep 12, 2024
e2e802e
Update Analyze.hs
sodic Sep 18, 2024
2d27900
Merge branch 'main' into filip-ts-sdk
sodic Sep 25, 2024
987243d
Remove todo for duplication
sodic Sep 25, 2024
15e3e68
Remove the duplication in templating
sodic Sep 25, 2024
7600288
Rearrange analyze.hs
sodic Sep 25, 2024
2b0c0ff
Remove todo after adding it to GitHub
sodic Sep 25, 2024
7ec9356
Remove one more todo
sodic Sep 25, 2024
1597f4b
Remove todo after adding it to GitHub
sodic Sep 25, 2024
b887cd8
Resolve some more todos
sodic Sep 25, 2024
1bdc871
Remove another todo
sodic Sep 25, 2024
52378c4
Send prisma models to JS runtime
sodic Sep 26, 2024
115dce5
Merge branch 'main' into filip-ts-sdk
sodic Sep 26, 2024
2617b6f
Properly parse decls.json
sodic Sep 26, 2024
32275e5
Improve error messages
sodic Sep 26, 2024
b673308
Merge branch 'main' into filip-ts-sdk
sodic Sep 30, 2024
3513b68
Fix ext import parsing and inject entities
sodic Oct 5, 2024
e9ef283
Fail if you find both wasp files
sodic Oct 7, 2024
0dc7435
Change tsconfig file name
sodic Oct 7, 2024
3e9efc8
Clean up imports
sodic Oct 7, 2024
24f65ec
Fix some more imports
sodic Oct 7, 2024
edd7a85
Change type name
sodic Oct 7, 2024
ae76820
Implement review changes
sodic Oct 7, 2024
7eab0bc
Fix failing tests
sodic Oct 7, 2024
8c8cb4f
Remove duplication when parsing ext imports
sodic Oct 7, 2024
d0ba9e5
Remove redundant variable
sodic Oct 7, 2024
4eec3c1
Remove trailling space
sodic Oct 7, 2024
1699df2
Revert Show instance for Decl
sodic Oct 7, 2024
5a6719e
Add module to package.json and remove mts
sodic Oct 8, 2024
3f48b98
Merge branch 'main' into filip-ts-sdk
sodic Oct 8, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import qualified Data.Text as T
import StrongPath (Abs, Dir, File, Path')
import Wasp.Cli.Command.CreateNewProject.Common (defaultWaspVersionBounds)
import Wasp.Cli.Command.CreateNewProject.ProjectDescription (NewProjectAppName, NewProjectName)
import Wasp.Project.Analyze (findPackageJsonFile, findWaspFile)
import Wasp.Project.Analyze (WaspFile (..), findPackageJsonFile, findWaspFile)
import Wasp.Project.Common (WaspProjectDir)
import qualified Wasp.Util.IO as IOUtil

Expand All @@ -26,7 +26,10 @@ replaceTemplatePlaceholdersInWaspFile ::
replaceTemplatePlaceholdersInWaspFile appName projectName projectDir =
findWaspFile projectDir >>= \case
Nothing -> return ()
Just absMainWaspFile -> replaceTemplatePlaceholdersInFileOnDisk appName projectName absMainWaspFile
Just (WaspLang absMainWaspFile) -> replaceTemplatePlaceholders absMainWaspFile
Just (WaspTs absMainTsFile) -> replaceTemplatePlaceholders absMainTsFile
where
replaceTemplatePlaceholders = replaceTemplatePlaceholdersInFileOnDisk appName projectName

-- | Template file for package.json file has placeholders in it that we want to replace
-- in the package.json file we have written to the disk.
Expand Down
5 changes: 3 additions & 2 deletions waspc/src/Wasp/Error.hs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
module Wasp.Error (showCompilerErrorForTerminal) where

import Data.List (intercalate)
import StrongPath (Abs, File', Path')
import StrongPath (Abs, Path')
import qualified StrongPath as SP
import StrongPath.Types (File)
import Wasp.Analyzer.Parser.Ctx (Ctx, getCtxRgn)
import Wasp.Analyzer.Parser.SourcePosition (SourcePosition (..))
import Wasp.Analyzer.Parser.SourceRegion (SourceRegion (..))
Expand All @@ -12,7 +13,7 @@ import qualified Wasp.Util.Terminal as T
-- | Transforms compiler error (error with parse context) into an informative, pretty String that
-- can be printed directly into the terminal. It uses terminal features like escape codes
-- (colors, styling, ...).
showCompilerErrorForTerminal :: (Path' Abs File', String) -> (String, Ctx) -> String
showCompilerErrorForTerminal :: (Path' Abs (File f), String) -> (String, Ctx) -> String
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't have something like data WaspFile, so that we could do Path' Abs (File WaspFile)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice catch. We didn't before, but we'll have it now.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll take care of this later. It requires dealing with cyclic dependencies.

showCompilerErrorForTerminal (waspFilePath, waspFileContent) (errMsg, errCtx) =
let srcRegion = getCtxRgn errCtx
in intercalate
Expand Down
148 changes: 137 additions & 11 deletions waspc/src/Wasp/Project/Analyze.hs
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file has a lot of diverse imports, indicating that it might want to be split into multiple files. I think we should split it probably -> we could certainly separate the logic that deals with WaspFile, and maybe then further split that into WaspLang and WaspTs logic, but not yet sure about that.

Copy link
Contributor Author

@sodic sodic Sep 30, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought about extracting a Project.WaspFile module but wasn't sure if it's necessary (thought I mentioned it in a comment somewhere, but looks like I didn't).

But yeah, if you like it too, I'll do it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leaving this one for later.

Original file line number Diff line number Diff line change
@@ -1,17 +1,26 @@
module Wasp.Project.Analyze
( analyzeWaspProject,
readPackageJsonFile,
analyzeWaspFileContent,
findWaspFile,
findPackageJsonFile,
analyzePrismaSchema,
WaspFile (..),
)
where

import Control.Applicative ((<|>))
import Control.Arrow (ArrowChoice (left))
import Control.Concurrent (newChan)
import Control.Concurrent.Async (concurrently)
import Control.Monad.Except (ExceptT (..), runExceptT)
import Control.Monad.IO.Class (liftIO)
import qualified Data.Aeson as Aeson
import Data.Conduit.Process.Typed (ExitCode (..))
import Data.List (find, isSuffixOf)
import StrongPath (Abs, Dir, File', Path', toFilePath, (</>))
import StrongPath (Abs, Dir, File', Path', Rel, toFilePath, (</>))
import qualified StrongPath as SP
import StrongPath.TH (relfile)
import StrongPath.Types (File)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see somebody went wild with accepting import suggestions from LS :D. You can just add those to StrongPath import above. I also wonder if you need separate qualified as SP line.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Heh.

I have had this item on my Haskell list for quite some time.

For some reason (and for some SP imports), LSP almost never suggests a top-level import from StrongPath. Whenever I look at LSP's import suggestions, this is my protocol:

  1. Check if there is an Add to import option.
  2. Check if there is a top-level import option (in this case, StrongPath).
  3. Use a lower-level import option and fix the import manually later (sometimes it works, sometimes it doesn't).

This mental overhead drives me crazy, and sometimes I forget to change the imports manually later (like here). I would really like to solve this because I don't want to waste time manually editing the imports.

@Martinsos @infomiho Is this specific to my setup, does it happen to you?
Please try with reldirP in Wasp.Project.Common. I just reproduced it there:

image

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Screenshot 2024-09-30 at 13 13 41

Top level for me

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am in emacs, so it is a bit different, but first I get a suggestion for autocompletion:

image

This one is correct, but when I accept, for some reason it doesn't also add an import automatically. So I get a compiler error that symbol is missing. I then tell LSP to suggest code actions, and then I get this

image

Which is correct! I am offered also the lower level imports, but the first option I am offered is to add it to existing import list of StrongPath, and if I pick that, it adds import correctly.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, glad to hear it's fixable because it's been annoying me for ages (particularly with StrongPath imports). I'll bother you in the office to help me figure it out.

import qualified Wasp.Analyzer as Analyzer
import Wasp.Analyzer.AnalyzeError (getErrorMessageAndCtx)
import Wasp.Analyzer.Parser.Ctx (Ctx)
Expand All @@ -23,10 +32,14 @@ import qualified Wasp.CompileOptions as CompileOptions
import qualified Wasp.ConfigFile as CF
import Wasp.Error (showCompilerErrorForTerminal)
import qualified Wasp.Generator.ConfigFile as G.CF
import qualified Wasp.Generator.Job as J
import Wasp.Generator.Job.IO (readJobMessagesAndPrintThemPrefixed)
import Wasp.Generator.Job.Process (runNodeCommandAsJob)
Comment on lines +43 to +45
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this is used here, then it sounds like it maybe shouldn't be in the Generator hm. What do you think? I didn't check the code though to see if there is anything Generator specific about it really or not.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, nice catch. I didn't notice it was there.

And there's nothing generator-specific about it. I think it's just there because the generator used to be the only one running jobs. I'll move it somewhere more appropriate.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leaving this one for later.

import Wasp.Project.Common
( CompileError,
CompileWarning,
WaspProjectDir,
dotWaspDirInWaspProjectDir,
findFileInWaspProjectDir,
packageJsonInWaspProjectDir,
prismaSchemaFileInWaspProjectDir,
Expand Down Expand Up @@ -60,7 +73,7 @@ analyzeWaspProject waspDir options = do
(Left prismaSchemaErrors, prismaSchemaWarnings) -> return (Left prismaSchemaErrors, prismaSchemaWarnings)
-- NOTE: we are ignoring prismaSchemaWarnings if the schema was parsed successfully
(Right prismaSchemaAst, _) ->
analyzeWaspFile prismaSchemaAst waspFilePath >>= \case
analyzeWaspFile waspDir prismaSchemaAst waspFilePath >>= \case
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

waspFilePath -> waspFile?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I prefer keeping the Path. I think it's our general convention too.

Left errors -> return (Left errors, [])
Right declarations ->
analyzePackageJsonContent waspDir >>= \case
Expand All @@ -69,8 +82,114 @@ analyzeWaspProject waspDir options = do
where
fileNotFoundMessage = "Couldn't find the *.wasp file in the " ++ toFilePath waspDir ++ " directory"

analyzeWaspFile :: Psl.Schema.Schema -> Path' Abs File' -> IO (Either [CompileError] [AS.Decl])
analyzeWaspFile prismaSchemaAst waspFilePath = do
data WaspFile
sodic marked this conversation as resolved.
Show resolved Hide resolved
= WaspLang !(Path' Abs (File WaspLangFile))
| WaspTs !(Path' Abs (File WaspTsFile))

data WaspLangFile

data WaspTsFile
Comment on lines +96 to +98
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if you went with data WaspFile = WaspLangFile | WaspTsFile and used DataKinds lang extension, assuming you previously renamed data WaspFile to data WaspFilePath which is any way a more correct name?
I believe that will allow you to then also do stuff like specify that a function takes any of these, because now you can use WaspLangFile as a kind also. I am not 100% sure because I haven't tried it and I am still a bit newer to type level programming but it might work nicely.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, can you elaborate how this works with the functions that use these types (compared to what we have now)?

I looked into DataKinds, and I don't think there's use for them here, but I might be missing something.


data CompiledWaspJsFile

data SpecJsonFile

analyzeWaspFile :: Path' Abs (Dir WaspProjectDir) -> Psl.Schema.Schema -> WaspFile -> IO (Either [CompileError] [AS.Decl])
analyzeWaspFile waspDir prismaSchemaAst = \case
WaspLang waspFilePath -> analyzeWaspLangFile prismaSchemaAst waspFilePath
WaspTs waspFilePath -> analyzeWaspTsFile waspDir prismaSchemaAst waspFilePath

analyzeWaspTsFile :: Path' Abs (Dir WaspProjectDir) -> Psl.Schema.Schema -> Path' Abs (File WaspTsFile) -> IO (Either [CompileError] [AS.Decl])
analyzeWaspTsFile waspProjectDir _prismaSchemaAst _waspFilePath = runExceptT $ do
-- TODO: The function currently doesn't require the path to main.wasp.ts
-- because it reads it from the tsconfig
-- Should we ensure that the tsconfig indeed points to the name we expect? Probably.
compiledWaspJsFile <- ExceptT $ compileWaspTsFile waspProjectDir
specJsonFile <- ExceptT $ executeMainWaspJsFile waspProjectDir compiledWaspJsFile
contents <- ExceptT $ readDeclsJsonFile specJsonFile
liftIO $ putStrLn "Here are the contents of the spec file:"
liftIO $ print contents
return []

-- TODO: Reconsider the return value. Can I write the function in such a way
-- that it's impossible to get the absolute path to the compiled file without
-- calling the function that compiles it?
-- To do that, I'd have to craete a private module that knows where the file is
-- and not expose the constant for creating the absoltue path (like I did with config/spec.json).
-- Normally, I could just put the constant in the where clause like I did there, but I'm hesitant
-- to do that since the path comes from the tsconfig.
-- That is what I did currently, but I'll have to figure out the long-term solution.
-- The ideal solution is reading the TS file, and passing its config to tsc
-- manually (and getting the output file path in the process).
compileWaspTsFile :: Path' Abs (Dir WaspProjectDir) -> IO (Either [CompileError] (Path' Abs (File CompiledWaspJsFile)))
compileWaspTsFile waspProjectDir = do
-- TODO: The function should also receive the tsconfig.node.json file (not the main.wasp.ts file),
-- because the source of truth (the name of the file, where it's compiled) comes from the typescript config.
-- However, we might want to keep this information in haskell and then verify that the tsconfig is correct.
chan <- newChan
(_, tscExitCode) <-
concurrently
(readJobMessagesAndPrintThemPrefixed chan)
( runNodeCommandAsJob
sodic marked this conversation as resolved.
Show resolved Hide resolved
sodic marked this conversation as resolved.
Show resolved Hide resolved
waspProjectDir
"npx"
[ "tsc",
"-p",
toFilePath (waspProjectDir </> tsconfigNodeFileInWaspProjectDir),
sodic marked this conversation as resolved.
Show resolved Hide resolved
"--noEmit",
"false",
"--outDir",
toFilePath $ SP.parent absCompiledWaspJsFile
]
J.Wasp
chan
)
case tscExitCode of
ExitFailure _status -> return $ Left ["Error while running TypeScript compiler on the *.wasp.mts file."]
ExitSuccess -> return $ Right absCompiledWaspJsFile
where
-- TODO: I should be getting the compiled file path from the tsconfig.node.file
absCompiledWaspJsFile = waspProjectDir </> dotWaspDirInWaspProjectDir </> [relfile|config/main.wasp.mjs|]
-- TODO: I'm not yet sure where this is going to come from because we also need
-- that knowledge to generate a TS SDK project.
tsconfigNodeFileInWaspProjectDir :: Path' (Rel WaspProjectDir) File'
tsconfigNodeFileInWaspProjectDir = [relfile|tsconfig.node.json|]

executeMainWaspJsFile :: Path' Abs (Dir WaspProjectDir) -> Path' Abs (File CompiledWaspJsFile) -> IO (Either [CompileError] (Path' Abs (File SpecJsonFile)))
executeMainWaspJsFile waspProjectDir absCompiledMainWaspJsFile = do
chan <- newChan
(_, runExitCode) <- do
concurrently
(readJobMessagesAndPrintThemPrefixed chan)
( runNodeCommandAsJob
waspProjectDir
"npx"
-- TODO: Figure out how to keep running instructions in a single place
-- (e.g., this is the same as the package name, but it's repeated in two places).
-- Before this, I had the entrypoint file hardcoded, which was bad
-- too: waspProjectDir </> [relfile|node_modules/wasp-config/dist/run.js|]
[ "wasp-config",
SP.fromAbsFile absCompiledMainWaspJsFile,
sodic marked this conversation as resolved.
Show resolved Hide resolved
SP.fromAbsFile absSpecOutputFile
]
J.Wasp
chan
)
case runExitCode of
ExitFailure _status -> return $ Left ["Error while running the compiled *.wasp.mts file."]
ExitSuccess -> return $ Right absSpecOutputFile
where
-- TODO: The config part of the path is problematic because it relies on TSC to create it during compilation,
-- see notes in compileWaspFile.
absSpecOutputFile = waspProjectDir </> dotWaspDirInWaspProjectDir </> [relfile|config/spec.json|]

readDeclsJsonFile :: Path' Abs (File SpecJsonFile) -> IO (Either [CompileError] Aeson.Value)
readDeclsJsonFile declsJsonFile = do
byteString <- IOUtil.readFile declsJsonFile
return $ Right $ Aeson.toJSON byteString

analyzeWaspLangFile :: Psl.Schema.Schema -> Path' Abs (File WaspLangFile) -> IO (Either [CompileError] [AS.Decl])
analyzeWaspLangFile prismaSchemaAst waspFilePath = do
waspFileContent <- IOUtil.readFile waspFilePath
left (map $ showCompilerErrorForTerminal (waspFilePath, waspFileContent))
<$> analyzeWaspFileContent prismaSchemaAst waspFileContent
Expand Down Expand Up @@ -118,15 +237,22 @@ constructAppSpec waspDir options packageJson parsedPrismaSchema decls = do

return $ runValidation ASV.validateAppSpec appSpec

findWaspFile :: Path' Abs (Dir WaspProjectDir) -> IO (Maybe (Path' Abs File'))
findWaspFile :: Path' Abs (Dir WaspProjectDir) -> IO (Maybe WaspFile)
findWaspFile waspDir = do
files <- fst <$> IOUtil.listDirectory waspDir
return $ (waspDir </>) <$> find isWaspFile files
return $ findWaspTsFile files <|> findWaspLangFile files
sodic marked this conversation as resolved.
Show resolved Hide resolved
where
isWaspFile path =
".wasp"
`isSuffixOf` toFilePath path
&& (length (toFilePath path) > length (".wasp" :: String))
findWaspTsFile files = WaspTs <$> findFileThatEndsWith ".wasp.mts" files
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I explained the mjs and mts story in Discord, you can ignore that for now.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If that sticks, we will also wnat to have a comment in code somewhere that explains it.

findWaspLangFile files = WaspLang <$> findFileThatEndsWith ".wasp" files
-- TODO: We used to have a check that made sure not to misidentify the .wasp
-- dir as a wasp file, but that's not needed (fst <$>
-- IOUtil.listDirectory already takes care of that and says so in its signature).
-- A bigger problem is if the user has a file with the same name as the wasp dir,
-- but that's a problem that should be solved in a different way (it's
-- still possible to have both main.wasp and .wasp files and cause that
-- error).
-- TODO: Try out what happens when Wasp finds this file, but the tsconfing setup and package are missing
findFileThatEndsWith suffix files = SP.castFile . (waspDir </>) <$> find ((suffix `isSuffixOf`) . toFilePath) files
infomiho marked this conversation as resolved.
Show resolved Hide resolved

analyzePackageJsonContent :: Path' Abs (Dir WaspProjectDir) -> IO (Either [CompileError] PackageJson)
analyzePackageJsonContent waspProjectDir =
Expand Down
5 changes: 3 additions & 2 deletions waspc/src/Wasp/Project/Common.hs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ module Wasp.Project.Common
where

import StrongPath (Abs, Dir, File', Path', Rel, reldir, relfile, toFilePath, (</>))
import StrongPath.Types (File)
import System.Directory (doesFileExist)
import Wasp.AppSpec.ExternalFiles (SourceExternalCodeDir, SourceExternalPublicDir)
import qualified Wasp.Generator.Common
Expand Down Expand Up @@ -86,8 +87,8 @@ tsconfigInWaspProjectDir = [relfile|tsconfig.json|]

findFileInWaspProjectDir ::
Path' Abs (Dir WaspProjectDir) ->
Path' (Rel WaspProjectDir) File' ->
IO (Maybe (Path' Abs File'))
Path' (Rel WaspProjectDir) (File f) ->
Martinsos marked this conversation as resolved.
Show resolved Hide resolved
IO (Maybe (Path' Abs (File f)))
findFileInWaspProjectDir waspDir file = do
let fileAbsFp = waspDir </> file
fileExists <- doesFileExist $ toFilePath fileAbsFp
Expand Down
Loading