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

Merged
merged 41 commits into from
Oct 10, 2024
Merged

Using TS SDK from waspc #2276

merged 41 commits into from
Oct 10, 2024

Conversation

sodic
Copy link
Contributor

@sodic sodic commented Sep 11, 2024

These PR contains the changes that enable waspc to process TS SDK projects that look like this.

.
├── main.wasp.mts         // Wasp config file
├── package.json
├── package-lock.json
├── public
├── schema.prisma
├── src
├── tsconfig.src.json     // tsconfig for src
├── tsconfig.json         // Composite  tsconfig
├── tsconfig.wasp.json    // tsconfig for the Wasp config file
├── vite.config.ts
└── node_moduels
    ├── ...
    └── wasp-config       // The package for the TS sdk

The PR doesn't contain any code for creating the artifacts necessary for such a project (e.g., tsconfig templates, the wasp-config package), etc. It's just the Haskell code required to process it.

It's just the purple parts from this graph (the prep work and the blue parts are coming in #2299). The idea is to review this PR by treating the blue parts as a black box with a simple runNodeCommandAsJob interface (but if you really really want a project to try it out, I can hook you up).

@Martinsos
Copy link
Member

image

@sodic sodic changed the title TS Sdk Using TS SDK from waspc Sep 26, 2024
waspc/src/Wasp/Project/Analyze.hs Outdated Show resolved Hide resolved
waspc/src/Wasp/Project/Analyze.hs Outdated Show resolved Hide resolved
waspc/src/Wasp/Project/Analyze.hs Show resolved Hide resolved
waspc/src/Wasp/Project/Analyze.hs Outdated Show resolved Hide resolved
waspc/src/Wasp/Project/Analyze.hs Show resolved Hide resolved
Comment on lines +55 to +56
getModelNames :: Schema -> [String]
getModelNames schema = map Model.getName $ getModels schema
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@Martinsos I realize all other functions in the file are just "getters" for the Schema fields and that this breaks module's "purity," but I strongly believe this function should exist (it decouples the caller from the schema's internals).

Where should I move it, if anywhere? I like it here, but I seem to recall you asking me to stick with the getter-only rule in the past.

Copy link
Contributor

Choose a reason for hiding this comment

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

There's always Wasp.Psl.Util which seems like an okay place to put this helper.

Copy link
Member

Choose a reason for hiding this comment

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

Well it is not so terrible, but I get what you mean: I mean it is quite random, why can't caller just do Model.getName <$> getModels schema? I don't see much value in decoupling caller from schema's internals -> the fact that schema has models in it are not internals, nor that each model has a name, it is basically its API.
The fact that you have a caller which needs just model names but doesn't care how exactly to fetch models or a name from a model doesn't seem like a good enough reason to create a whole new API here for that use case.

So I would say, either move this function to wherever you need it (so much close to the caller and out of Wasp/Psl module subtree), or even better, get rid of this function and just inline it wherever you are using it -> again, I don't think you gain anyting by abstracting stuff here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Model.getName <$> getModels schema

This is the part I'm not a fan of. We do that often (inline accesses to source-of-truth objects), but it always bugs me a little.

My main motivation here is Law of Demeter.

The counter-argument (if I got it correctly) is that, in this case, the schema's internal structure is so inherent that there's no point in hiding it (and that's why this particular instance is not the best example, but bear with me).

I generally dislike chaining multiple "accessor" calls. It requires importing multiple symbols, and refactoring many imports and call sites whenever I make a simple name change.

I'm talking about the stuff like:

import Foo
import Bar
import AppSpec

... Foo.getBar <$> Foo.getBars $ AS.getFoo spec

-- then later again

... Foo.getBar <$> Foo.getBars $ AS.getFoo spec

Sometimes someone notices the repetition and defines a local helper function (like you suggest here). Then, in a different location, someone else notices the repetition again and defines an identical helper function (again local). We end up with N inline queries and M local functions that all do the same thing.

For example, this getModelNames function is used in two modules: Wasp.Project.Analyze and Wasp.LSP.Prisma.

Ideally, If I want something from the AppSpec in the generator, I don't want to import the entire hierarchy and inline the same expression multiple times.
I want to import only a single function that gives me what I want and then use that.

In simple terms, I'd prefer if our source-of-truth objects (like the AppSpec and the schema) had a higher-level API for stuff that's often used. It would be perfect if we could use an index file to reexport stuff we want to expose and "ban" all lower-level imports (but I don't think Haskell can do this).

Copy link
Member

@Martinsos Martinsos Oct 1, 2024

Choose a reason for hiding this comment

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

That makes sense to me, but these accessors can then get quite specific. What I don't like then is that I am looking at lets say AppSpec code, and I see these couple of quite custom accessors, which are on higher level. But then, for other stuff in the AppSpec, there are no such higher level accessors. Which is good I guess, because there would have to be a bunch of them to have a complete "higher level api", but on the other hand, why did we pick these ones then? Answer is, because they were used a lot. So that means that for less used stuff, we are still uisng lower level API? So now we are deciding if API is public or not based on frequency of usage?

And yeah, what you said about inherency of the structure is important. Or I would say, inherency of the API. Or, I would say, how public is API. If API is public, I don't see an issue with it being somewhat lower level.

Sounds like you are solving decoupling problem above, but it is not really a problem because we intend that API to be used like that, there is no "implementation" leaking out, but you introduce the new problem of adding higher level API but only a bit of it which I think starts a mess.

The case when this makes sense, I think, is if

  1. It is really implementation that is leaking.
  2. There is opportunity to introduce complete higher level API, not pieces of it.

Otherwise, if we need only piece of that higher level API, I think it is indeed better to have it defined at the place of usage. If you don't like that one being redefined on multiple places, well you can make a common place where it is defined so those callers can share it -> it still doesn't have to be part of the AppSpec.

Btw, I might be ok with it if it was some more complex stuff. Some common operation that uses multiple lower level mechanisms. But then it would be ok, because it would probably not be just a piece of higher level API, it would be a complete higher level API for that thing. Here, when we have just the combining of accessors for public parts of the structure, there is so many combos that it doesn't make much sense to pick just a few combos and claim that you did a decoupling.

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'm leaving this one for later as well.

".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.

waspc/test/AppSpec/FromJSONTest.hs Show resolved Hide resolved
Comment on lines +159 to +163
-- TODO: Figure out how to keep running instructions in a single
-- place (e.g., this is string 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|]
Copy link
Contributor Author

@sodic sodic Sep 26, 2024

Choose a reason for hiding this comment

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

This is a topic for the next PR (the one that sets up the package): #2299

Comment on lines +106 to +107
-- TODO: I'm not yet sure where tsconfig.node.json location should come from
-- because we also need that knowledge when generating a TS SDK project.
Copy link
Contributor Author

@sodic sodic Sep 26, 2024

Choose a reason for hiding this comment

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

This is a topic for the next PR (the one that sets up the package): #2299

@sodic sodic marked this pull request as ready for review September 26, 2024 12:12
@infomiho
Copy link
Contributor

LGTM, I'm looking forwards to reviewing other bits and testing this locally!

I'm eager to hear if we can get some IDE support for users when writing entities.

Copy link
Member

@Martinsos Martinsos left a comment

Choose a reason for hiding this comment

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

Nice stuff @sodic I am super excited about this!

Biggest comments are:

  1. We could probably do with more comments (function header comments likely) in this code as it is quite complex / has design desicions that are not obivous from the code.
  2. I didn't really understand the reasoning behind the comment referring to tsc call, I commented more on that there.

And the rest is covered by all the specific comments.

waspc/src/Wasp/Project/Analyze.hs Outdated Show resolved Hide resolved
@@ -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.

Comment on lines +35 to +37
import qualified Wasp.Generator.Job as J
import Wasp.Generator.Job.IO (readJobMessagesAndPrintThemPrefixed)
import Wasp.Generator.Job.Process (runNodeCommandAsJob)
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.

Comment on lines 21 to 22
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.

waspc/src/Wasp/Project/Analyze.hs Outdated Show resolved Hide resolved
waspc/src/Wasp/Project/Analyze.hs Outdated Show resolved Hide resolved
".wasp"
`isSuffixOf` toFilePath path
&& (length (toFilePath path) > length (".wasp" :: String))
findWaspTsFile files = WaspTs <$> findFileThatEndsWith ".wasp.mts" files
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.

waspc/src/Wasp/Project/Common.hs Show resolved Hide resolved
Comment on lines +55 to +56
getModelNames :: Schema -> [String]
getModelNames schema = map Model.getName $ getModels schema
Copy link
Member

Choose a reason for hiding this comment

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

Well it is not so terrible, but I get what you mean: I mean it is quite random, why can't caller just do Model.getName <$> getModels schema? I don't see much value in decoupling caller from schema's internals -> the fact that schema has models in it are not internals, nor that each model has a name, it is basically its API.
The fact that you have a caller which needs just model names but doesn't care how exactly to fetch models or a name from a model doesn't seem like a good enough reason to create a whole new API here for that use case.

So I would say, either move this function to wherever you need it (so much close to the caller and out of Wasp/Psl module subtree), or even better, get rid of this function and just inline it wherever you are using it -> again, I don't think you gain anyting by abstracting stuff here.

waspc/waspls/src/Wasp/LSP/Prisma/Util.hs Outdated Show resolved Hide resolved
Comment on lines +167 to +172
getEntityDecls :: Psl.Schema.Schema -> Either [AnalyzeError] [Decl]
getEntityDecls schema =
wrapAnalyzerError TypeError (typeCheck stdTypes astWithEntitiesOnly)
>>= (wrapAnalyzerError EvaluationError . evaluate stdTypes)
where
astWithEntitiesOnly = Parser.AST $ parseEntityStatements schema
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@Martinsos This is what we discussed on Discord: sending only the entity statements through the compilation pipeline to get their declarations.

Should I document the idea behind it?

Comment on lines +164 to +166
case AppSpec.ExtImport.parseExtImportPath extImportPath of
Left err -> mkParseError ctx err
Right importPath -> pure $ AppSpec.ExtImport.ExtImport name importPath
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 needed this for FromJSON so I moved it.

Comment on lines +37 to +39
extImportPath <- case parseExtImportPath pathStr of
Right path' -> pure path'
Left err -> fail err
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@Martinsos Any helper function for this? I can't use left because of fail.

tsConfigInWaspProjectDir :: Path' (Rel WaspProjectDir) (File TsConfigFile)
tsConfigInWaspProjectDir = [relfile|tsconfig.json|]
tsConfigInWaspProjectDir = [relfile|tsconfig.app.json|]
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is a problem, fix it.

@@ -156,33 +156,18 @@ tuple4 eval1 eval2 eval3 eval4 = evaluation $ \(typeDefs, bindings) -> withCtx $
extImport :: TypedExprEvaluation AppSpec.ExtImport.ExtImport
extImport = evaluation' . withCtx $ \ctx -> \case
TypedAST.ExtImport name extImportPath ->
-- NOTE(martin): This parsing here could instead be done in Parser.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Remove this

@sodic sodic changed the base branch from main to ts-sdk-preview October 9, 2024 14:34
@sodic sodic merged commit d90b235 into ts-sdk-preview Oct 10, 2024
12 checks passed
@sodic sodic deleted the filip-ts-sdk branch October 10, 2024 07:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants