From 4c9e7e3b3af60946411b7e1125a64fa674dbf3ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Sodi=C4=87?= Date: Thu, 2 Jan 2025 14:57:52 +0100 Subject: [PATCH] Clean up TS config technical debt (#2435) --- .../StarterTemplates/Templating.hs | 4 +- waspc/cli/src/Wasp/Cli/Command/Db/Studio.hs | 2 +- .../cli/src/Wasp/Cli/Command/TsConfigSetup.hs | 6 +- waspc/packages/wasp-config/eslint.config.js | 20 +- waspc/packages/wasp-config/src/appSpec.ts | 189 ++++++++++------- ...appers.ts => mapUserSpecToAppSpecDecls.ts} | 197 +++++++++--------- waspc/packages/wasp-config/src/run.ts | 89 ++++---- waspc/packages/wasp-config/src/userApi.ts | 19 +- waspc/packages/wasp-config/tsconfig.json | 1 - .../Wasp/AI/GenerateNewProject/WaspFile.hs | 2 +- waspc/src/Wasp/Analyzer.hs | 5 + .../Evaluation/TypedExpr/Combinators.hs | 1 - waspc/src/Wasp/AppSpec.hs | 4 + waspc/src/Wasp/AppSpec/ExtImport.hs | 4 +- waspc/src/Wasp/Error.hs | 3 +- waspc/src/Wasp/Generator/DbGenerator/Jobs.hs | 4 +- .../Wasp/Generator/DbGenerator/Operations.hs | 4 +- waspc/src/Wasp/Generator/NpmInstall.hs | 9 +- waspc/src/Wasp/Generator/SdkGenerator.hs | 6 +- .../Wasp/Generator/ServerGenerator/Setup.hs | 4 +- .../Wasp/Generator/ServerGenerator/Start.hs | 4 +- waspc/src/Wasp/Generator/Start.hs | 4 +- waspc/src/Wasp/Generator/Test.hs | 2 +- .../Wasp/Generator/WebAppGenerator/Setup.hs | 4 +- .../Wasp/Generator/WebAppGenerator/Start.hs | 4 +- .../Wasp/Generator/WebAppGenerator/Test.hs | 4 +- waspc/src/Wasp/{Generator => }/Job.hs | 2 +- waspc/src/Wasp/{Generator => }/Job/Common.hs | 4 +- waspc/src/Wasp/{Generator => }/Job/IO.hs | 8 +- .../{Generator => }/Job/IO/PrefixedWriter.hs | 8 +- waspc/src/Wasp/{Generator => }/Job/Process.hs | 4 +- waspc/src/Wasp/NodePackageFFI.hs | 7 +- waspc/src/Wasp/Project/Analyze.hs | 157 +------------- waspc/src/Wasp/Project/Common.hs | 1 - waspc/src/Wasp/Project/WaspFile.hs | 57 +++++ waspc/src/Wasp/Project/WaspFile/TypeScript.hs | 166 +++++++++++++++ waspc/src/Wasp/Project/WaspFile/WaspLang.hs | 35 ++++ waspc/src/Wasp/Psl/Ast/Schema.hs | 5 - waspc/src/Wasp/Util.hs | 4 + waspc/tools/install_packages_to_data_dir.sh | 3 + waspc/waspc.cabal | 15 +- waspc/waspls/src/Wasp/LSP/Prisma/Analyze.hs | 5 +- 42 files changed, 631 insertions(+), 445 deletions(-) rename waspc/packages/wasp-config/src/{mappers.ts => mapUserSpecToAppSpecDecls.ts} (76%) rename waspc/src/Wasp/{Generator => }/Job.hs (96%) rename waspc/src/Wasp/{Generator => }/Job/Common.hs (90%) rename waspc/src/Wasp/{Generator => }/Job/IO.hs (85%) rename waspc/src/Wasp/{Generator => }/Job/IO/PrefixedWriter.hs (97%) rename waspc/src/Wasp/{Generator => }/Job/Process.hs (98%) create mode 100644 waspc/src/Wasp/Project/WaspFile.hs create mode 100644 waspc/src/Wasp/Project/WaspFile/TypeScript.hs create mode 100644 waspc/src/Wasp/Project/WaspFile/WaspLang.hs diff --git a/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates/Templating.hs b/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates/Templating.hs index 0dc791d2bd..968aacd1f9 100644 --- a/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates/Templating.hs +++ b/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates/Templating.hs @@ -10,9 +10,10 @@ import StrongPath (Abs, Dir, File, Path') import Wasp.Cli.Command.CreateNewProject.Common (defaultWaspVersionBounds) import Wasp.Cli.Command.CreateNewProject.ProjectDescription (NewProjectAppName, NewProjectName) import Wasp.NodePackageFFI (InstallablePackage (WaspConfigPackage), getPackageInstallationPath) -import Wasp.Project.Analyze (WaspFilePath (..), findWaspFile) +import Wasp.Project.Analyze (WaspFilePath (..)) import Wasp.Project.Common (WaspProjectDir) import Wasp.Project.ExternalConfig.PackageJson (findPackageJsonFile) +import Wasp.Project.WaspFile (findWaspFile) import qualified Wasp.Util.IO as IOUtil replaceTemplatePlaceholdersInTemplateFiles :: NewProjectAppName -> NewProjectName -> Path' Abs (Dir WaspProjectDir) -> IO () @@ -52,7 +53,6 @@ replaceTemplatePlaceholdersInFileOnDisk appName projectName file = do ("__waspProjectName__", show projectName), ("__waspVersion__", defaultWaspVersionBounds) ] - -- TODO: We do this in all files, but not all files have all placeholders updateFileContentWith (replacePlaceholders waspTemplateReplacements) file where updateFileContentWith :: (Text -> Text) -> Path' Abs (File f) -> IO () diff --git a/waspc/cli/src/Wasp/Cli/Command/Db/Studio.hs b/waspc/cli/src/Wasp/Cli/Command/Db/Studio.hs index 09db8ca458..91abc66c03 100644 --- a/waspc/cli/src/Wasp/Cli/Command/Db/Studio.hs +++ b/waspc/cli/src/Wasp/Cli/Command/Db/Studio.hs @@ -11,7 +11,7 @@ import Wasp.Cli.Command (Command) import Wasp.Cli.Command.Message (cliSendMessageC) import Wasp.Cli.Command.Require (InWaspProject (InWaspProject), require) import Wasp.Generator.DbGenerator.Jobs (runStudio) -import Wasp.Generator.Job.IO (readJobMessagesAndPrintThemPrefixed) +import Wasp.Job.IO (readJobMessagesAndPrintThemPrefixed) import qualified Wasp.Message as Msg import Wasp.Project.Common (dotWaspDirInWaspProjectDir, generatedCodeDirInDotWaspDir) diff --git a/waspc/cli/src/Wasp/Cli/Command/TsConfigSetup.hs b/waspc/cli/src/Wasp/Cli/Command/TsConfigSetup.hs index beaf6328b8..62751a780b 100644 --- a/waspc/cli/src/Wasp/Cli/Command/TsConfigSetup.hs +++ b/waspc/cli/src/Wasp/Cli/Command/TsConfigSetup.hs @@ -8,9 +8,9 @@ import StrongPath (Abs, Dir, Path') import System.Exit (ExitCode (..)) import Wasp.Cli.Command (Command, CommandError (..), require) import Wasp.Cli.Command.Require (InWaspProject (InWaspProject)) -import qualified Wasp.Generator.Job as J -import Wasp.Generator.Job.IO (readJobMessagesAndPrintThemPrefixed) -import Wasp.Generator.Job.Process (runNodeCommandAsJob) +import qualified Wasp.Job as J +import Wasp.Job.IO (readJobMessagesAndPrintThemPrefixed) +import Wasp.Job.Process (runNodeCommandAsJob) import Wasp.NodePackageFFI (InstallablePackage (WaspConfigPackage), getPackageInstallationPath) -- | Prepares the project for using Wasp's TypeScript SDK. diff --git a/waspc/packages/wasp-config/eslint.config.js b/waspc/packages/wasp-config/eslint.config.js index 8a580849a5..e8d837ff71 100644 --- a/waspc/packages/wasp-config/eslint.config.js +++ b/waspc/packages/wasp-config/eslint.config.js @@ -1,11 +1,10 @@ -import globals from "globals"; -import pluginJs from "@eslint/js"; -import tseslint from "typescript-eslint"; +import globals from 'globals' +import pluginJs from '@eslint/js' +import tseslint from 'typescript-eslint' export default [ pluginJs.configs.recommended, ...tseslint.configs.strict, - // Todo: explore typed-linting: https://typescript-eslint.io/getting-started/typed-linting { languageOptions: { globals: globals.node, @@ -13,14 +12,15 @@ export default [ }, // global ignore { - ignores: ["node_modules/", "dist/"], + ignores: ['node_modules/', 'dist/'], }, { rules: { - "@typescript-eslint/no-unused-vars": "warn", - "@typescript-eslint/no-empty-function": "warn", - "no-empty": "warn", - "no-constant-condition": "warn", + '@typescript-eslint/no-unused-vars': 'warn', + '@typescript-eslint/no-empty-function': 'warn', + 'no-empty': 'warn', + 'no-constant-condition': 'warn', + 'object-shorthand': 'warn', }, }, -]; +] diff --git a/waspc/packages/wasp-config/src/appSpec.ts b/waspc/packages/wasp-config/src/appSpec.ts index 074c620c28..3742119513 100644 --- a/waspc/packages/wasp-config/src/appSpec.ts +++ b/waspc/packages/wasp-config/src/appSpec.ts @@ -1,20 +1,33 @@ -/** This module is a mirror implementation of AppSpec Decls in TypeScript. - * The original implemention is in Haskell (waspc). +/** This module is a mirror implementation of FromJSON for AppSpec Decls in + * TypeScript. The original implemention is in Haskell (waspc). * * IMPORTANT: Do not change this file without updating the AppSpec in waspc. */ -export type Decl = - | { declType: 'App'; declName: string; declValue: App } - | { declType: 'Page'; declName: string; declValue: Page } - | { declType: 'Route'; declName: string; declValue: Route } - | { declType: 'Query'; declName: string; declValue: Query } - | { declType: 'Action'; declName: string; declValue: Action } - | { declType: 'App'; declName: string; declValue: App } - | { declType: 'Job'; declName: string; declValue: Job } - | { declType: 'Api'; declName: string; declValue: Api } - | { declType: 'ApiNamespace'; declName: string; declValue: ApiNamespace } - | { declType: 'Crud'; declName: string; declValue: Crud } +export type Decl = { + [Type in keyof DeclTypeToValue]: { + declType: Type + declName: string + declValue: DeclTypeToValue[Type] + } +}[keyof DeclTypeToValue] + +export type DeclTypeToValue = { + App: App + Page: Page + Route: Route + Query: Query + Action: Action + Job: Job + Api: Api + ApiNamespace: ApiNamespace + Crud: Crud +} + +export type GetDeclForType = Extract< + Decl, + { declType: T } +> // NOTE: Entities are defined in the schema.prisma file, but they can still be // referenced. @@ -22,7 +35,7 @@ export type DeclType = Decl['declType'] | 'Entity' export type Page = { component: ExtImport - authRequired?: boolean + authRequired: Optional } export type Route = { @@ -32,39 +45,39 @@ export type Route = { export type Action = { fn: ExtImport - entities?: Ref<'Entity'>[] - auth?: boolean + entities: Optional[]> + auth: Optional } export type Query = { fn: ExtImport - entities?: Ref<'Entity'>[] - auth?: boolean + entities: Optional[]> + auth: Optional } export type Job = { executor: JobExecutor perform: Perform - schedule?: Schedule - entities?: Ref<'Entity'>[] + schedule: Optional + entities: Optional[]> } export type Schedule = { cron: string - args?: object - executorOptions?: ExecutorOptions + args: Optional + executorOptions: Optional } export type Perform = { fn: ExtImport - executorOptions?: ExecutorOptions + executorOptions: Optional } export type Api = { fn: ExtImport - middlewareConfigFn?: ExtImport - entities?: Ref<'Entity'>[] + middlewareConfigFn: Optional + entities: Optional[]> httpRoute: HttpRoute - auth?: boolean + auth: Optional } export type ApiNamespace = { @@ -80,13 +93,13 @@ export type Crud = { export type App = { wasp: Wasp title: string - head?: string[] - auth?: Auth - server?: Server - client?: Client - db?: Db - emailSender?: EmailSender - webSocket?: WebSocket + head: Optional + auth: Optional + server: Optional + client: Optional + db: Optional + emailSender: Optional + webSocket: Optional } export type ExtImport = { @@ -98,7 +111,7 @@ export type ExtImport = { export type JobExecutor = 'PgBoss' export type ExecutorOptions = { - pgBoss?: object + pgBoss: Optional } export type HttpMethod = 'ALL' | 'GET' | 'POST' | 'PUT' | 'DELETE' @@ -106,56 +119,55 @@ export type HttpMethod = 'ALL' | 'GET' | 'POST' | 'PUT' | 'DELETE' export type HttpRoute = [HttpMethod, string] export type CrudOperations = { - get?: CrudOperationOptions - getAll?: CrudOperationOptions - create?: CrudOperationOptions - update?: CrudOperationOptions - delete?: CrudOperationOptions + get: Optional + getAll: Optional + create: Optional + update: Optional + delete: Optional } export type CrudOperationOptions = { - isPublic?: boolean - overrideFn?: ExtImport + isPublic: Optional + overrideFn: Optional } export type Wasp = { - // TODO: Check semver in export type system? version: string } export type Auth = { userEntity: Ref<'Entity'> - externalAuthEntity?: Ref<'Entity'> + externalAuthEntity: Optional> methods: AuthMethods onAuthFailedRedirectTo: string - onAuthSucceededRedirectTo?: string - onBeforeSignup?: ExtImport - onAfterSignup?: ExtImport - onBeforeOAuthRedirect?: ExtImport - onBeforeLogin?: ExtImport - onAfterLogin?: ExtImport + onAuthSucceededRedirectTo: Optional + onBeforeSignup: Optional + onAfterSignup: Optional + onBeforeOAuthRedirect: Optional + onBeforeLogin: Optional + onAfterLogin: Optional } export type AuthMethods = { - usernameAndPassword?: UsernameAndPasswordConfig - discord?: ExternalAuthConfig - google?: ExternalAuthConfig - gitHub?: ExternalAuthConfig - keycloak?: ExternalAuthConfig - email?: EmailAuthConfig + usernameAndPassword: Optional + discord: Optional + google: Optional + gitHub: Optional + keycloak: Optional + email: Optional } export type UsernameAndPasswordConfig = { - userSignupFields?: ExtImport + userSignupFields: Optional } export type ExternalAuthConfig = { - configFn?: ExtImport - userSignupFields?: ExtImport + configFn: Optional + userSignupFields: Optional } export type EmailAuthConfig = { - userSignupFields?: ExtImport + userSignupFields: Optional fromField: EmailFromField emailVerification: EmailVerificationConfig passwordReset: PasswordResetConfig @@ -163,24 +175,23 @@ export type EmailAuthConfig = { export type EmailSender = { provider: EmailProvider - defaultFrom?: EmailFromField + defaultFrom: Optional } -// TODO: duplication export type EmailProvider = 'SMTP' | 'SendGrid' | 'Mailgun' | 'Dummy' export type EmailFromField = { - name?: string + name: Optional email: string } export type EmailVerificationConfig = { - getEmailContentFn?: ExtImport + getEmailContentFn: Optional clientRoute: Ref<'Route'> } export type PasswordResetConfig = { - getEmailContentFn?: ExtImport + getEmailContentFn: Optional clientRoute: Ref<'Route'> } @@ -190,21 +201,57 @@ export type Ref = { } export type Server = { - setupFn?: ExtImport - middlewareConfigFn?: ExtImport + setupFn: Optional + middlewareConfigFn: Optional } export type Client = { - setupFn?: ExtImport - rootComponent?: ExtImport - baseDir?: `/${string}` + setupFn: Optional + rootComponent: Optional + baseDir: Optional<`/${string}`> } export type Db = { - seeds?: ExtImport[] + seeds: Optional } export type WebSocket = { fn: ExtImport - autoConnect?: boolean + autoConnect: Optional } + +/** + * We use this type for fields that are optional (Maybe) in AppSpec. + * We do this instead of `someField?:` because we want TypeScript to force us + * to explicitly set the field to `undefined`. + * + * This way, if the AppSpec changes on the Haskell side, we won't forget to + * implement a proper mapping in TypeScript. + * + * For example, let's say `bar` is optional (both for the user and for the app + * spec). This would be the correct mapping code: + * ``` + * const { foo, bar } = userConfig + * const decl: SomeDecl = { + * foo: mapForAppSpec(foo), + * bar: mapForAppSpec(bar) + * } + * ``` + * The code below is wrong. It forgets to map `bar` even though it might exist + * in `userConfig`: + * ``` + * const { foo } = userConfig + * const decl: SomeDecl = { + * foo: mapForAppSpec(foo), + * } + * ``` + * If `bar` is an optional field of `SomeDecl` (`bar?: string`), TypeScript + * doesn't catch this error. + * + * If `bar` is a mandatory field of `SomeDecl` that can be set to `undefined` + * (`bar: Optional`), TypeScript catches the error. + * + * Explicitly setting optional fields to `undefined` doesn't impact JSON + * serialization since fields set to `undefined` are treated as missing fields. + */ +type Optional = T | undefined diff --git a/waspc/packages/wasp-config/src/mappers.ts b/waspc/packages/wasp-config/src/mapUserSpecToAppSpecDecls.ts similarity index 76% rename from waspc/packages/wasp-config/src/mappers.ts rename to waspc/packages/wasp-config/src/mapUserSpecToAppSpecDecls.ts index 711846b8b7..f611531154 100644 --- a/waspc/packages/wasp-config/src/mappers.ts +++ b/waspc/packages/wasp-config/src/mapUserSpecToAppSpecDecls.ts @@ -3,7 +3,7 @@ import * as AppSpec from './appSpec.js' import * as User from './userApi.js' -export function mapUserSpecToDecls( +export function mapUserSpecToAppSpecDecls( spec: User.UserSpec, entityNames: string[] ): AppSpec.Decl[] { @@ -22,36 +22,43 @@ export function mapUserSpecToDecls( routes, server, websocket, + cruds, } = spec const pageNames = Array.from(pages.keys()) const routeNames = Array.from(routes.keys()) + const parseEntityRef = makeRefParser('Entity', entityNames) const parsePageRef = makeRefParser('Page', pageNames) const parseRouteRef = makeRefParser('Route', routeNames) - // TODO: Try to build the entire object at once - const decls: AppSpec.Decl[] = [] - - // TODO: Find a way to make sure you've covered everything in compile time - for (const [pageName, pageConfig] of pages.entries()) { - decls.push({ - declType: 'Page', - declName: pageName, - declValue: mapPage(pageConfig), - }) - } - - for (const [routeName, routeConfig] of routes.entries()) { - decls.push({ - declType: 'Route', - declName: routeName, - declValue: mapRoute(routeConfig, parsePageRef), - }) - } - - decls.push({ - declType: 'App', + const pageDecls = mapToDecls(pages, 'Page', mapPage) + const routeDecls = mapToDecls(routes, 'Route', (routeConfig) => + mapRoute(routeConfig, parsePageRef) + ) + const actionDecls = mapToDecls(actions, 'Action', (actionConfig) => + mapOperationConfig(actionConfig, parseEntityRef) + ) + const queryDecls = mapToDecls(queries, 'Query', (queryConfig) => + mapOperationConfig(queryConfig, parseEntityRef) + ) + const apiDecls = mapToDecls(apis, 'Api', (apiConfig) => + mapApiConfig(apiConfig, parseEntityRef) + ) + const jobDecls = mapToDecls(jobs, 'Job', (jobConfig) => + mapJob(jobConfig, parseEntityRef) + ) + const apiNamespaceDecls = mapToDecls( + apiNamespaces, + 'ApiNamespace', + mapApiNamespace + ) + const crudDecls = mapToDecls(cruds, 'Crud', (crudConfig) => + mapCrud(crudConfig, parseEntityRef) + ) + + const appDecl = { + declType: 'App' as const, declName: app.name, declValue: mapApp( app.config, @@ -64,60 +71,39 @@ export function mapUserSpecToDecls( emailSender, websocket ), - }) - - for (const [actionName, actionConfig] of actions.entries()) { - decls.push({ - declType: 'Action', - declName: actionName, - declValue: mapOperationConfig(actionConfig, parseEntityRef), - }) - } - - for (const [queryName, queryConfig] of queries.entries()) { - decls.push({ - declType: 'Query', - declName: queryName, - declValue: mapOperationConfig(queryConfig, parseEntityRef), - }) } - for (const [apiName, apiConfig] of apis.entries()) { - decls.push({ - declType: 'Api', - declName: apiName, - declValue: mapApiConfig(apiConfig, parseEntityRef), - }) - } - - for (const [jobName, jobConfig] of jobs.entries()) { - decls.push({ - declType: 'Job', - declName: jobName, - declValue: mapJob(jobConfig, parseEntityRef), - }) - } - - for (const [ - apiNamespaceName, - apiNamespaceConfig, - ] of apiNamespaces.entries()) { - decls.push({ - declType: 'ApiNamespace', - declName: apiNamespaceName, - declValue: mapApiNamespace(apiNamespaceConfig), - }) - } + return makeDeclsArray({ + App: [appDecl], + Page: pageDecls, + Route: routeDecls, + Action: actionDecls, + Query: queryDecls, + Api: apiDecls, + Job: jobDecls, + ApiNamespace: apiNamespaceDecls, + Crud: crudDecls, + }) +} - for (const [crudName, crudConfig] of spec.cruds.entries()) { - decls.push({ - declType: 'Crud', - declName: crudName, - declValue: mapCrud(crudConfig, parseEntityRef), - }) - } +function makeDeclsArray(decls: { + [Type in AppSpec.Decl['declType']]: AppSpec.GetDeclForType[] +}): AppSpec.Decl[] { + return Object.values(decls).flatMap((decl) => [...decl]) +} - return decls +function mapToDecls( + configs: Map, + type: DeclType, + configToDeclValue: ( + config: T + ) => AppSpec.GetDeclForType['declValue'] +) { + return [...configs].map(([name, config]) => ({ + declType: type, + declName: name, + declValue: configToDeclValue(config), + })) } function mapOperationConfig( @@ -132,12 +118,11 @@ function mapOperationConfig( config: User.ActionConfig | User.QueryConfig, parseEntityRef: RefParser<'Entity'> ): AppSpec.Action | AppSpec.Query { - // TODO: How to make sure I've destructured everything? const { fn, entities, auth } = config return { fn: mapExtImport(fn), - ...(entities && { entities: entities.map(parseEntityRef) }), - auth: auth, + entities: entities && entities.map(parseEntityRef), + auth, } } @@ -151,7 +136,6 @@ function mapExtImport(extImport: User.ExtImport): AppSpec.ExtImport { path: extImport.from, } } else { - const _exhaustiveCheck: never = extImport throw new Error( 'Invalid ExtImport: neither `import` nor `importDefault` is defined' ) @@ -166,9 +150,9 @@ function mapApiConfig( return { fn: mapExtImport(fn), middlewareConfigFn: middlewareConfigFn && mapExtImport(middlewareConfigFn), - ...(entities && { entities: entities.map(parseEntityRef) }), - httpRoute: httpRoute, - auth: auth, + entities: entities && entities.map(parseEntityRef), + httpRoute, + auth, } } @@ -228,9 +212,10 @@ function mapAuth( return { userEntity: parseEntityRef(userEntity), // TODO: Abstract away this pattern - ...(externalAuthEntity && { - externalAuthEntity: parseEntityRef(externalAuthEntity), - }), + externalAuthEntity: + externalAuthEntity === undefined + ? undefined + : parseEntityRef(externalAuthEntity), methods: mapAuthMethods(methods, parseRouteRef), onAuthFailedRedirectTo, onAuthSucceededRedirectTo, @@ -270,7 +255,7 @@ function mapUsernameAndPassword( } } -export function mapExternalAuth( +function mapExternalAuth( externalAuth: User.ExternalAuthConfig ): AppSpec.ExternalAuthConfig { const { configFn, userSignupFields } = externalAuth @@ -281,14 +266,21 @@ export function mapExternalAuth( } function mapEmailAuth( - email: User.EmailAuthConfig, + emailConfig: User.EmailAuthConfig, parseRouteRef: RefParser<'Route'> ): AppSpec.EmailAuthConfig { - const { userSignupFields, fromField, emailVerification, passwordReset } = - email + const { + userSignupFields, + fromField: { name, email }, + emailVerification, + passwordReset, + } = emailConfig return { userSignupFields: userSignupFields && mapExtImport(userSignupFields), - fromField, + fromField: { + name, + email, + }, emailVerification: mapEmailVerification(emailVerification, parseRouteRef), passwordReset: mapPasswordReset(passwordReset, parseRouteRef), } @@ -305,7 +297,7 @@ function mapEmailVerification( } } -export function mapPasswordReset( +function mapPasswordReset( passwordReset: User.PasswordResetConfig, parseRouteRef: RefParser<'Route'> ): AppSpec.PasswordResetConfig { @@ -326,25 +318,30 @@ function mapDb(db: User.DbConfig): AppSpec.Db { function mapEmailSender( emailSender: User.EmailSenderConfig ): AppSpec.EmailSender { - return emailSender + const { provider, defaultFrom } = emailSender + return { + provider, + defaultFrom: defaultFrom && { + name: defaultFrom.name, + email: defaultFrom.email, + }, + } } function mapServer(server: User.ServerConfig): AppSpec.Server { const { setupFn, middlewareConfigFn } = server return { - ...(setupFn && { setupFn: mapExtImport(setupFn) }), - ...(middlewareConfigFn && { - middlewareConfigFn: mapExtImport(middlewareConfigFn), - }), + setupFn: setupFn && mapExtImport(setupFn), + middlewareConfigFn: middlewareConfigFn && mapExtImport(middlewareConfigFn), } } function mapClient(client: User.ClientConfig): AppSpec.Client { const { setupFn, rootComponent, baseDir } = client return { - ...(setupFn && { setupFn: mapExtImport(setupFn) }), - ...(rootComponent && { rootComponent: mapExtImport(rootComponent) }), - ...(baseDir && { baseDir }), + setupFn: setupFn && mapExtImport(setupFn), + rootComponent: rootComponent && mapExtImport(rootComponent), + baseDir, } } @@ -362,10 +359,10 @@ function mapJob( ): AppSpec.Job { const { executor, perform, schedule, entities } = job return { - executor: executor, + executor, perform: mapPerform(perform), schedule: schedule && mapSchedule(schedule), - ...(entities && { entities: entities.map(parseEntityRef) }), + entities: entities && entities.map(parseEntityRef), } } @@ -381,7 +378,7 @@ function mapPerform(perform: User.Perform): AppSpec.Perform { const { fn, executorOptions } = perform return { fn: mapExtImport(fn), - ...(executorOptions && { executorOptions }), + executorOptions, } } diff --git a/waspc/packages/wasp-config/src/run.ts b/waspc/packages/wasp-config/src/run.ts index b810dc08bf..a172d48d35 100644 --- a/waspc/packages/wasp-config/src/run.ts +++ b/waspc/packages/wasp-config/src/run.ts @@ -2,51 +2,70 @@ import { writeFileSync } from 'fs' import { App } from './userApi.js' import { Decl } from './appSpec.js' -import { mapUserSpecToDecls } from './mappers.js' +import { mapUserSpecToAppSpecDecls } from './mapUserSpecToAppSpecDecls.js' import { GET_USER_SPEC } from './_private.js' -import { exit } from 'process' main() async function main() { - const { mainWaspJs, outputFile, entityNames } = parseProcessArguments( - process.argv - ) + const { + mainWaspJs, + outputFile: declsJsonOutputFile, + entityNames, + } = parseProcessArgsOrThrow(process.argv) - const app = await importApp(mainWaspJs) - const spec = analyzeApp(app, entityNames) + const result = await getAppDefinitionOrError(mainWaspJs) + if (result.status === 'error') { + console.error(result.error) + process.exit(1) + } + const { value: appDefinition } = result - writeFileSync(outputFile, serialize(spec)) -} + const decls = analyzeAppDefinition(appDefinition, entityNames) + const declsJson = getDeclsJson(decls) -async function importApp(mainWaspJs: string): Promise { - const app: unknown = (await import(mainWaspJs)).default - if (!app) { - console.error( - 'Could not load your app config. Make sure your *.wasp.ts file includes a default export of the app.' - ) - exit(1) - } - if (!isApp(app)) { - console.error( - 'The default export of your *.wasp.ts file must be an instance of App.' - ) - console.error('Make sure you export an object created with new App(...).') - exit(1) - } - return app + writeFileSync(declsJsonOutputFile, declsJson) } -function isApp(app: unknown): app is App { - return app instanceof App +async function getAppDefinitionOrError( + mainWaspJs: string +): Promise> { + const usersDefaultExport: unknown = (await import(mainWaspJs)).default + return getValidAppOrError(usersDefaultExport) } -function analyzeApp(app: App, entityNames: string[]): Decl[] { +function analyzeAppDefinition(app: App, entityNames: string[]): Decl[] { const userSpec = app[GET_USER_SPEC]() - return mapUserSpecToDecls(userSpec, entityNames) + return mapUserSpecToAppSpecDecls(userSpec, entityNames) +} + +function getDeclsJson(appConfig: Decl[]): string { + return JSON.stringify(appConfig) } -function parseProcessArguments(args: string[]): { +function getValidAppOrError(app: unknown): Result { + if (!app) { + return { + status: 'error', + error: + 'Could not load your app config. ' + + 'Make sure your *.wasp.ts file includes a default export of the app.', + } + } + + if (!(app instanceof App)) { + return { + status: 'error', + error: + 'The default export of your *.wasp.ts file must be an instance of App. ' + + 'Make sure you export an object created with new App(...).', + } + } + + return { status: 'ok', value: app } +} + +function parseProcessArgsOrThrow(args: string[]): { mainWaspJs: string outputFile: string entityNames: string[] @@ -68,7 +87,7 @@ function parseProcessArguments(args: string[]): { ) } - const entityNames = parseEntityNamesJson(entityNamesJson) + const entityNames = getValidEntityNamesOrThrow(entityNamesJson) return { mainWaspJs, @@ -77,7 +96,7 @@ function parseProcessArguments(args: string[]): { } } -function parseEntityNamesJson(entitiesJson: string): string[] { +function getValidEntityNamesOrThrow(entitiesJson: string): string[] { const entities = JSON.parse(entitiesJson) if (!Array.isArray(entities)) { throw new Error('The entities JSON must be an array of entity names.') @@ -85,6 +104,6 @@ function parseEntityNamesJson(entitiesJson: string): string[] { return entities } -function serialize(appConfig: Decl[]): string { - return JSON.stringify(appConfig) -} +type Result = + | { status: 'ok'; value: Value } + | { status: 'error'; error: Error } diff --git a/waspc/packages/wasp-config/src/userApi.ts b/waspc/packages/wasp-config/src/userApi.ts index 211c358a21..02291b58ed 100644 --- a/waspc/packages/wasp-config/src/userApi.ts +++ b/waspc/packages/wasp-config/src/userApi.ts @@ -6,14 +6,16 @@ import { GET_USER_SPEC } from './_private.js' export class App { #userSpec: UserSpec; - // NOTE: Using a non-public symbol gives us a pacakge-private property. + // NOTE: Using a non-public symbol gives us a package-private property. + // It's not that important to hide it from the users, but we still don't want + // user's IDE to suggest it during autocompletion. [GET_USER_SPEC]() { return this.#userSpec } constructor(name: string, config: AppConfig) { this.#userSpec = { - app: { name, config: config }, + app: { name, config }, actions: new Map(), apiNamespaces: new Map(), apis: new Map(), @@ -90,9 +92,11 @@ export class App { } } -export type WaspConfig = AppSpec.Wasp - -export type AppConfig = Pick +export type AppConfig = { + title: string + wasp: AppSpec.Wasp + head?: string[] +} export type ExtImport = | { @@ -203,7 +207,10 @@ export type QueryConfig = { auth?: boolean } -export type EmailSenderConfig = AppSpec.EmailSender +export type EmailSenderConfig = { + provider: AppSpec.EmailProvider + defaultFrom?: EmailFromField +} export type AuthConfig = { userEntity: string diff --git a/waspc/packages/wasp-config/tsconfig.json b/waspc/packages/wasp-config/tsconfig.json index 0224b8b38b..759d826c99 100644 --- a/waspc/packages/wasp-config/tsconfig.json +++ b/waspc/packages/wasp-config/tsconfig.json @@ -20,6 +20,5 @@ "declaration": true, "lib": ["es2022"] }, - // better structure in output "include": ["src"], } diff --git a/waspc/src/Wasp/AI/GenerateNewProject/WaspFile.hs b/waspc/src/Wasp/AI/GenerateNewProject/WaspFile.hs index 3d0ef8f76d..c164015a1e 100644 --- a/waspc/src/Wasp/AI/GenerateNewProject/WaspFile.hs +++ b/waspc/src/Wasp/AI/GenerateNewProject/WaspFile.hs @@ -24,7 +24,7 @@ import qualified Wasp.AI.GenerateNewProject.Common.Prompts as Prompts import Wasp.AI.GenerateNewProject.Plan (Plan) import Wasp.AI.OpenAI.ChatGPT (ChatMessage (..), ChatRole (..)) import Wasp.Analyzer.Parser.Ctx (Ctx (..)) -import Wasp.Project.Analyze (analyzeWaspFileContent) +import Wasp.Project.WaspFile.WaspLang (analyzeWaspFileContent) import qualified Wasp.Psl.Ast.Schema as Psl.Schema import qualified Wasp.Util.Aeson as Utils.Aeson diff --git a/waspc/src/Wasp/Analyzer.hs b/waspc/src/Wasp/Analyzer.hs index 8c4c98a68a..2d32232ede 100644 --- a/waspc/src/Wasp/Analyzer.hs +++ b/waspc/src/Wasp/Analyzer.hs @@ -166,6 +166,11 @@ analyze prismaSchemaAst = getEntityDecls :: Psl.Schema.Schema -> Either [AnalyzeError] [Decl] getEntityDecls schema = + -- Since Wasp's AST includes entity declarations, the easiest way to get a list + -- of all entities defined in the Prisma Schema is by: + -- 1. Creating an AST with (and only with) the declarations for the Prisma + -- schema Entities. + -- 2. Type-checking that AST and returning the result. wrapAnalyzerError TypeError (typeCheck stdTypes astWithEntitiesOnly) >>= (wrapAnalyzerError EvaluationError . evaluate stdTypes) where diff --git a/waspc/src/Wasp/Analyzer/Evaluator/Evaluation/TypedExpr/Combinators.hs b/waspc/src/Wasp/Analyzer/Evaluator/Evaluation/TypedExpr/Combinators.hs index 410b8f19b3..0b2bef955b 100644 --- a/waspc/src/Wasp/Analyzer/Evaluator/Evaluation/TypedExpr/Combinators.hs +++ b/waspc/src/Wasp/Analyzer/Evaluator/Evaluation/TypedExpr/Combinators.hs @@ -154,7 +154,6 @@ 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. -- NOTE(martin): This parsing here could instead be done in Parser. -- I don't have a very good reason for doing it here instead of Parser, except -- for being somewhat simpler to implement. diff --git a/waspc/src/Wasp/AppSpec.hs b/waspc/src/Wasp/AppSpec.hs index 03087a37b9..44b4196601 100644 --- a/waspc/src/Wasp/AppSpec.hs +++ b/waspc/src/Wasp/AppSpec.hs @@ -58,6 +58,10 @@ import qualified Wasp.SemanticVersion as SV -- describing the web app specification with all the details needed to generate it. -- It is standalone and de-coupled from other parts of the compiler and knows nothing about them, -- instead other parts are using it: Analyzer produces AppSpec while Generator consumes it. +-- +-- IMPORTANT: Do not change this data structure without updating the AppSpec in +-- packages/wasp-config/src/appSpec.ts. That module is a TypeScript mirror +-- implementation of AppSpec's FromJSON. data AppSpec = AppSpec { -- | List of declarations like App, Page, Route, ... that describe the web app. decls :: [Decl], diff --git a/waspc/src/Wasp/AppSpec/ExtImport.hs b/waspc/src/Wasp/AppSpec/ExtImport.hs index fee7606a78..165030fd99 100644 --- a/waspc/src/Wasp/AppSpec/ExtImport.hs +++ b/waspc/src/Wasp/AppSpec/ExtImport.hs @@ -34,9 +34,7 @@ instance FromJSON ExtImport where nameStr <- o .: "name" pathStr <- o .: "path" extImportName <- parseExtImportName kindStr nameStr - extImportPath <- case parseExtImportPath pathStr of - Right path' -> pure path' - Left err -> fail err + extImportPath <- either fail pure $ parseExtImportPath pathStr return $ ExtImport extImportName extImportPath where parseExtImportName kindStr nameStr = case kindStr of diff --git a/waspc/src/Wasp/Error.hs b/waspc/src/Wasp/Error.hs index aa4ef8474a..a02cb070f6 100644 --- a/waspc/src/Wasp/Error.hs +++ b/waspc/src/Wasp/Error.hs @@ -7,13 +7,14 @@ import StrongPath.Types (File) import Wasp.Analyzer.Parser.Ctx (Ctx, getCtxRgn) import Wasp.Analyzer.Parser.SourcePosition (SourcePosition (..)) import Wasp.Analyzer.Parser.SourceRegion (SourceRegion (..)) +import Wasp.Project.Common (WaspLangFile) import Wasp.Util (indent, insertAt, leftPad) 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 f), String) -> (String, Ctx) -> String +showCompilerErrorForTerminal :: (Path' Abs (File WaspLangFile), String) -> (String, Ctx) -> String showCompilerErrorForTerminal (waspFilePath, waspFileContent) (errMsg, errCtx) = let srcRegion = getCtxRgn errCtx in intercalate diff --git a/waspc/src/Wasp/Generator/DbGenerator/Jobs.hs b/waspc/src/Wasp/Generator/DbGenerator/Jobs.hs index 0687b23a28..bb93ec3fc6 100644 --- a/waspc/src/Wasp/Generator/DbGenerator/Jobs.hs +++ b/waspc/src/Wasp/Generator/DbGenerator/Jobs.hs @@ -19,10 +19,10 @@ import StrongPath.TH (relfile) import qualified System.Info import Wasp.Generator.Common (ProjectRootDir) import Wasp.Generator.DbGenerator.Common (MigrateArgs (..), dbSchemaFileInProjectRootDir) -import qualified Wasp.Generator.Job as J -import Wasp.Generator.Job.Process (runNodeCommandAsJob, runNodeCommandAsJobWithExtraEnv) import Wasp.Generator.ServerGenerator.Common (serverRootDirInProjectRootDir) import Wasp.Generator.ServerGenerator.Db.Seed (dbSeedNameEnvVarName) +import qualified Wasp.Job as J +import Wasp.Job.Process (runNodeCommandAsJob, runNodeCommandAsJobWithExtraEnv) import Wasp.Project.Common (WaspProjectDir, waspProjectDirFromProjectRootDir) migrateDev :: Path' Abs (Dir ProjectRootDir) -> MigrateArgs -> J.Job diff --git a/waspc/src/Wasp/Generator/DbGenerator/Operations.hs b/waspc/src/Wasp/Generator/DbGenerator/Operations.hs index 503e912938..1b18d8d207 100644 --- a/waspc/src/Wasp/Generator/DbGenerator/Operations.hs +++ b/waspc/src/Wasp/Generator/DbGenerator/Operations.hs @@ -36,12 +36,12 @@ import Wasp.Generator.DbGenerator.Common ) import qualified Wasp.Generator.DbGenerator.Jobs as DbJobs import Wasp.Generator.FileDraft.WriteableMonad (WriteableMonad (copyDirectoryRecursive, doesDirectoryExist)) -import Wasp.Generator.Job.IO +import qualified Wasp.Generator.WriteFileDrafts as Generator.WriteFileDrafts +import Wasp.Job.IO ( collectJobTextOutputUntilExitReceived, printJobMsgsUntilExitReceived, readJobMessagesAndPrintThemPrefixed, ) -import qualified Wasp.Generator.WriteFileDrafts as Generator.WriteFileDrafts import Wasp.Project.Db.Migrations (DbMigrationsDir) import Wasp.Util (checksumFromFilePath, hexToString) import Wasp.Util.IO (deleteFileIfExists, doesFileExist) diff --git a/waspc/src/Wasp/Generator/NpmInstall.hs b/waspc/src/Wasp/Generator/NpmInstall.hs index 560525dce1..095f5d323c 100644 --- a/waspc/src/Wasp/Generator/NpmInstall.hs +++ b/waspc/src/Wasp/Generator/NpmInstall.hs @@ -1,6 +1,5 @@ module Wasp.Generator.NpmInstall - ( installProjectNpmDependencies, - installNpmDependenciesWithInstallRecord, + ( installNpmDependenciesWithInstallRecord, ) where @@ -17,15 +16,15 @@ import System.Exit (ExitCode (..)) import UnliftIO (race) import Wasp.AppSpec (AppSpec (waspProjectDir)) import Wasp.Generator.Common (ProjectRootDir) -import Wasp.Generator.Job (Job, JobMessage, JobType) -import qualified Wasp.Generator.Job as J -import Wasp.Generator.Job.IO.PrefixedWriter (PrefixedWriter, printJobMessagePrefixed, runPrefixedWriter) import Wasp.Generator.Monad (GeneratorError (..)) import Wasp.Generator.NpmInstall.Common (AllNpmDeps (..), getAllNpmDeps) import Wasp.Generator.NpmInstall.InstalledNpmDepsLog (forgetInstalledNpmDepsLog, loadInstalledNpmDepsLog, saveInstalledNpmDepsLog) import qualified Wasp.Generator.SdkGenerator as SdkGenerator import qualified Wasp.Generator.ServerGenerator.Setup as ServerSetup import qualified Wasp.Generator.WebAppGenerator.Setup as WebAppSetup +import Wasp.Job (Job, JobMessage, JobType) +import qualified Wasp.Job as J +import Wasp.Job.IO.PrefixedWriter (PrefixedWriter, printJobMessagePrefixed, runPrefixedWriter) import Wasp.Project.Common (WaspProjectDir, nodeModulesDirInWaspProjectDir) import qualified Wasp.Util.IO as IOUitl diff --git a/waspc/src/Wasp/Generator/SdkGenerator.hs b/waspc/src/Wasp/Generator/SdkGenerator.hs index ad0e8a1b12..259d1f97e3 100644 --- a/waspc/src/Wasp/Generator/SdkGenerator.hs +++ b/waspc/src/Wasp/Generator/SdkGenerator.hs @@ -37,9 +37,6 @@ import Wasp.Generator.DbGenerator (getEntitiesForPrismaSchema) import qualified Wasp.Generator.DbGenerator.Auth as DbAuth import Wasp.Generator.FileDraft (FileDraft) import qualified Wasp.Generator.FileDraft as FD -import qualified Wasp.Generator.Job as J -import Wasp.Generator.Job.IO (readJobMessagesAndPrintThemPrefixed) -import Wasp.Generator.Job.Process (runNodeCommandAsJob) import Wasp.Generator.Monad (Generator) import qualified Wasp.Generator.NpmDependencies as N import Wasp.Generator.SdkGenerator.AuthG (genAuth) @@ -66,6 +63,9 @@ import Wasp.Generator.WebAppGenerator.Common reactVersion, ) import qualified Wasp.Generator.WebAppGenerator.Common as WebApp +import qualified Wasp.Job as J +import Wasp.Job.IO (readJobMessagesAndPrintThemPrefixed) +import Wasp.Job.Process (runNodeCommandAsJob) import qualified Wasp.Node.Version as NodeVersion import Wasp.Project.Common (WaspProjectDir) import qualified Wasp.Project.Db as Db diff --git a/waspc/src/Wasp/Generator/ServerGenerator/Setup.hs b/waspc/src/Wasp/Generator/ServerGenerator/Setup.hs index 03194b729f..b0e44f571a 100644 --- a/waspc/src/Wasp/Generator/ServerGenerator/Setup.hs +++ b/waspc/src/Wasp/Generator/ServerGenerator/Setup.hs @@ -5,9 +5,9 @@ where import StrongPath (Abs, Dir, Path', ()) import Wasp.Generator.Common (ProjectRootDir) -import qualified Wasp.Generator.Job as J -import Wasp.Generator.Job.Process (runNodeCommandAsJob) import qualified Wasp.Generator.ServerGenerator.Common as Common +import qualified Wasp.Job as J +import Wasp.Job.Process (runNodeCommandAsJob) installNpmDependencies :: Path' Abs (Dir ProjectRootDir) -> J.Job installNpmDependencies projectDir = do diff --git a/waspc/src/Wasp/Generator/ServerGenerator/Start.hs b/waspc/src/Wasp/Generator/ServerGenerator/Start.hs index 1f5dfd95bf..a2edb34f87 100644 --- a/waspc/src/Wasp/Generator/ServerGenerator/Start.hs +++ b/waspc/src/Wasp/Generator/ServerGenerator/Start.hs @@ -5,9 +5,9 @@ where import StrongPath (Abs, Dir, Path', ()) import Wasp.Generator.Common (ProjectRootDir) -import qualified Wasp.Generator.Job as J -import Wasp.Generator.Job.Process (runNodeCommandAsJob) import qualified Wasp.Generator.ServerGenerator.Common as Common +import qualified Wasp.Job as J +import Wasp.Job.Process (runNodeCommandAsJob) startServer :: Path' Abs (Dir ProjectRootDir) -> J.Job startServer projectDir = do diff --git a/waspc/src/Wasp/Generator/Start.hs b/waspc/src/Wasp/Generator/Start.hs index 43cb9b8c4a..95c9848056 100644 --- a/waspc/src/Wasp/Generator/Start.hs +++ b/waspc/src/Wasp/Generator/Start.hs @@ -9,10 +9,10 @@ import Control.Concurrent.Extra (threadDelay) import Control.Monad (void) import StrongPath (Abs, Dir, Path') import Wasp.Generator.Common (ProjectRootDir) -import qualified Wasp.Generator.Job as J -import Wasp.Generator.Job.IO (readJobMessagesAndPrintThemPrefixed) import Wasp.Generator.ServerGenerator.Start (startServer) import Wasp.Generator.WebAppGenerator.Start (startWebApp) +import qualified Wasp.Job as J +import Wasp.Job.IO (readJobMessagesAndPrintThemPrefixed) -- | This is a blocking action, that will start the processes that run web app and server. -- It will run as long as one of those processes does not fail. diff --git a/waspc/src/Wasp/Generator/Test.hs b/waspc/src/Wasp/Generator/Test.hs index c266f0ec3b..cffdd87e9c 100644 --- a/waspc/src/Wasp/Generator/Test.hs +++ b/waspc/src/Wasp/Generator/Test.hs @@ -7,8 +7,8 @@ import Control.Concurrent (newChan) import Control.Concurrent.Async (concurrently) import StrongPath (Abs, Dir, Path') import System.Exit (ExitCode (..)) -import Wasp.Generator.Job.IO (readJobMessagesAndPrintThemPrefixed) import qualified Wasp.Generator.WebAppGenerator.Test as WebAppTest +import Wasp.Job.IO (readJobMessagesAndPrintThemPrefixed) import Wasp.Project.Common (WaspProjectDir) testWebApp :: [String] -> Path' Abs (Dir WaspProjectDir) -> IO (Either String ()) diff --git a/waspc/src/Wasp/Generator/WebAppGenerator/Setup.hs b/waspc/src/Wasp/Generator/WebAppGenerator/Setup.hs index 4e82d4d399..ec5082ea81 100644 --- a/waspc/src/Wasp/Generator/WebAppGenerator/Setup.hs +++ b/waspc/src/Wasp/Generator/WebAppGenerator/Setup.hs @@ -5,9 +5,9 @@ where import StrongPath (Abs, Dir, Path', ()) import Wasp.Generator.Common (ProjectRootDir) -import qualified Wasp.Generator.Job as J -import Wasp.Generator.Job.Process (runNodeCommandAsJob) import qualified Wasp.Generator.WebAppGenerator.Common as Common +import qualified Wasp.Job as J +import Wasp.Job.Process (runNodeCommandAsJob) installNpmDependencies :: Path' Abs (Dir ProjectRootDir) -> J.Job installNpmDependencies projectDir = do diff --git a/waspc/src/Wasp/Generator/WebAppGenerator/Start.hs b/waspc/src/Wasp/Generator/WebAppGenerator/Start.hs index 188560ee81..6bd126f6b3 100644 --- a/waspc/src/Wasp/Generator/WebAppGenerator/Start.hs +++ b/waspc/src/Wasp/Generator/WebAppGenerator/Start.hs @@ -5,9 +5,9 @@ where import StrongPath (Abs, Dir, Path', ()) import Wasp.Generator.Common (ProjectRootDir) -import qualified Wasp.Generator.Job as J -import Wasp.Generator.Job.Process (runNodeCommandAsJob) import qualified Wasp.Generator.WebAppGenerator.Common as Common +import qualified Wasp.Job as J +import Wasp.Job.Process (runNodeCommandAsJob) startWebApp :: Path' Abs (Dir ProjectRootDir) -> J.Job startWebApp projectDir = do diff --git a/waspc/src/Wasp/Generator/WebAppGenerator/Test.hs b/waspc/src/Wasp/Generator/WebAppGenerator/Test.hs index 6755395503..fd832ca554 100644 --- a/waspc/src/Wasp/Generator/WebAppGenerator/Test.hs +++ b/waspc/src/Wasp/Generator/WebAppGenerator/Test.hs @@ -5,9 +5,9 @@ where import StrongPath (Abs, Dir, Path', relfile, ()) import qualified StrongPath as SP -import qualified Wasp.Generator.Job as J -import Wasp.Generator.Job.Process (runNodeCommandAsJob) import Wasp.Generator.WebAppGenerator.Common (webAppRootDirInProjectRootDir) +import qualified Wasp.Job as J +import Wasp.Job.Process (runNodeCommandAsJob) import Wasp.Project.Common (WaspProjectDir, dotWaspDirInWaspProjectDir, generatedCodeDirInDotWaspDir) testWebApp :: [String] -> Path' Abs (Dir WaspProjectDir) -> J.Job diff --git a/waspc/src/Wasp/Generator/Job.hs b/waspc/src/Wasp/Job.hs similarity index 96% rename from waspc/src/Wasp/Generator/Job.hs rename to waspc/src/Wasp/Job.hs index 45730c29a2..ac34fcaceb 100644 --- a/waspc/src/Wasp/Generator/Job.hs +++ b/waspc/src/Wasp/Job.hs @@ -1,4 +1,4 @@ -module Wasp.Generator.Job +module Wasp.Job ( Job, JobMessage (..), JobMessageData (..), diff --git a/waspc/src/Wasp/Generator/Job/Common.hs b/waspc/src/Wasp/Job/Common.hs similarity index 90% rename from waspc/src/Wasp/Generator/Job/Common.hs rename to waspc/src/Wasp/Job/Common.hs index a979e475ff..8d6b046e9e 100644 --- a/waspc/src/Wasp/Generator/Job/Common.hs +++ b/waspc/src/Wasp/Job/Common.hs @@ -1,4 +1,4 @@ -module Wasp.Generator.Job.Common +module Wasp.Job.Common ( getJobMessageOutHandle, getJobMessageContent, ) @@ -7,7 +7,7 @@ where import qualified Data.Text as T import System.Exit (ExitCode (..)) import System.IO (Handle, stderr, stdout) -import qualified Wasp.Generator.Job as J +import qualified Wasp.Job as J getJobMessageOutHandle :: J.JobMessage -> Handle getJobMessageOutHandle jobMsg = case J._data jobMsg of diff --git a/waspc/src/Wasp/Generator/Job/IO.hs b/waspc/src/Wasp/Job/IO.hs similarity index 85% rename from waspc/src/Wasp/Generator/Job/IO.hs rename to waspc/src/Wasp/Job/IO.hs index ef563eb8c7..54fe3b8ca3 100644 --- a/waspc/src/Wasp/Generator/Job/IO.hs +++ b/waspc/src/Wasp/Job/IO.hs @@ -1,4 +1,4 @@ -module Wasp.Generator.Job.IO +module Wasp.Job.IO ( readJobMessagesAndPrintThemPrefixed, printJobMessage, printJobMsgsUntilExitReceived, @@ -11,9 +11,9 @@ import Control.Monad.IO.Class (liftIO) import Data.Text (Text) import qualified Data.Text.IO as T.IO import System.IO (hFlush) -import qualified Wasp.Generator.Job as J -import Wasp.Generator.Job.Common (getJobMessageContent, getJobMessageOutHandle) -import Wasp.Generator.Job.IO.PrefixedWriter (printJobMessagePrefixed, runPrefixedWriter) +import qualified Wasp.Job as J +import Wasp.Job.Common (getJobMessageContent, getJobMessageOutHandle) +import Wasp.Job.IO.PrefixedWriter (printJobMessagePrefixed, runPrefixedWriter) printJobMsgsUntilExitReceived :: Chan J.JobMessage -> IO () printJobMsgsUntilExitReceived chan = do diff --git a/waspc/src/Wasp/Generator/Job/IO/PrefixedWriter.hs b/waspc/src/Wasp/Job/IO/PrefixedWriter.hs similarity index 97% rename from waspc/src/Wasp/Generator/Job/IO/PrefixedWriter.hs rename to waspc/src/Wasp/Job/IO/PrefixedWriter.hs index d3f3bd0d97..8e0325553b 100644 --- a/waspc/src/Wasp/Generator/Job/IO/PrefixedWriter.hs +++ b/waspc/src/Wasp/Job/IO/PrefixedWriter.hs @@ -1,7 +1,7 @@ {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE TupleSections #-} -module Wasp.Generator.Job.IO.PrefixedWriter +module Wasp.Job.IO.PrefixedWriter ( printJobMessagePrefixed, runPrefixedWriter, PrefixedWriter, @@ -17,9 +17,9 @@ import qualified Data.Set as S import qualified Data.Text as T import qualified Data.Text.IO as T.IO import System.IO (hFlush, stderr) -import Wasp.Generator.Job (JobType) -import qualified Wasp.Generator.Job as J -import Wasp.Generator.Job.Common (getJobMessageContent, getJobMessageOutHandle) +import Wasp.Job (JobType) +import qualified Wasp.Job as J +import Wasp.Job.Common (getJobMessageContent, getJobMessageOutHandle) import qualified Wasp.Util.Terminal as Term -- | diff --git a/waspc/src/Wasp/Generator/Job/Process.hs b/waspc/src/Wasp/Job/Process.hs similarity index 98% rename from waspc/src/Wasp/Generator/Job/Process.hs rename to waspc/src/Wasp/Job/Process.hs index 136fdc0215..c4f96bd0e8 100644 --- a/waspc/src/Wasp/Generator/Job/Process.hs +++ b/waspc/src/Wasp/Job/Process.hs @@ -1,6 +1,6 @@ {-# LANGUAGE ScopedTypeVariables #-} -module Wasp.Generator.Job.Process +module Wasp.Job.Process ( runProcessAsJob, runNodeCommandAsJob, runNodeCommandAsJobWithExtraEnv, @@ -21,7 +21,7 @@ import System.Exit (ExitCode (..)) import qualified System.Info import qualified System.Process as P import UnliftIO.Exception (bracket) -import qualified Wasp.Generator.Job as J +import qualified Wasp.Job as J import qualified Wasp.Node.Version as NodeVersion -- TODO: diff --git a/waspc/src/Wasp/NodePackageFFI.hs b/waspc/src/Wasp/NodePackageFFI.hs index f2ef7ff312..439eadf774 100644 --- a/waspc/src/Wasp/NodePackageFFI.hs +++ b/waspc/src/Wasp/NodePackageFFI.hs @@ -23,6 +23,8 @@ import Wasp.Data (DataDir) import qualified Wasp.Data as Data import qualified Wasp.Node.Version as NodeVersion +-- | This are the globally installed packages waspc runs directly from +-- their global installation path. data RunnablePackage = DeployPackage | TsInspectPackage @@ -36,6 +38,9 @@ data RunnablePackage PrismaPackage | WaspStudioPackage +-- | This are the globally installed packages waspc installs into +-- the user's project using `npm`. They are used/run from inside the project's +-- node_modules. data InstallablePackage = WaspConfigPackage data PackagesDir @@ -69,8 +74,6 @@ scriptInPackageDir = [relfile|dist/index.js|] -- If the package does not have its dependencies installed yet (for example, -- when the package is run for the first time after installing Wasp), we install -- the dependencies. --- TODO: How would it not have npm dependencies installed if we always to it in --- install_packages_to_data_dir.sh? getPackageProcessOptions :: RunnablePackage -> [String] -> IO P.CreateProcess getPackageProcessOptions package args = do NodeVersion.getAndCheckUserNodeVersion >>= \case diff --git a/waspc/src/Wasp/Project/Analyze.hs b/waspc/src/Wasp/Project/Analyze.hs index 0004fad4c9..235c29df26 100644 --- a/waspc/src/Wasp/Project/Analyze.hs +++ b/waspc/src/Wasp/Project/Analyze.hs @@ -1,56 +1,30 @@ module Wasp.Project.Analyze ( analyzeWaspProject, - analyzeWaspFileContent, - findWaspFile, analyzePrismaSchema, WaspFilePath (..), ) where import Control.Arrow (ArrowChoice (left)) -import Control.Concurrent (newChan) -import Control.Concurrent.Async (concurrently) -import Control.Monad.Except (ExceptT (..), liftEither, runExceptT) -import qualified Data.Aeson as Aeson -import Data.List (find, isSuffixOf) import StrongPath ( Abs, Dir, - File, File', Path', - Rel, - basename, - castFile, fromAbsDir, - fromAbsFile, - fromRelFile, - relfile, - (), ) -import System.Exit (ExitCode (..)) -import qualified Wasp.Analyzer as Analyzer -import Wasp.Analyzer.AnalyzeError (getErrorMessageAndCtx) -import Wasp.Analyzer.Parser.Ctx (Ctx) import qualified Wasp.AppSpec as AS import Wasp.AppSpec.Core.Decl.JSON () import qualified Wasp.AppSpec.Valid as ASV import Wasp.CompileOptions (CompileOptions) 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) import Wasp.Project.Common ( CompileError, CompileWarning, WaspFilePath (..), - WaspLangFile, WaspProjectDir, - WaspTsFile, - dotWaspDirInWaspProjectDir, findFileInWaspProjectDir, getSrcTsConfigInWaspProjectDir, prismaSchemaFileInWaspProjectDir, @@ -62,13 +36,12 @@ import Wasp.Project.Env (readDotEnvClient, readDotEnvServer) import qualified Wasp.Project.ExternalConfig as EC import qualified Wasp.Project.ExternalFiles as ExternalFiles import Wasp.Project.Vite (findCustomViteConfigPath) +import Wasp.Project.WaspFile (analyzeWaspFile, findWaspFile) import qualified Wasp.Psl.Ast.Schema as Psl.Schema import qualified Wasp.Psl.Parser.Schema as Psl.Parser import Wasp.Psl.Valid (getValidDbSystemFromPrismaSchema) import qualified Wasp.Psl.Valid as PslV -import Wasp.Util.Aeson (encodeToString) import qualified Wasp.Util.IO as IOUtil -import Wasp.Util.StrongPath (replaceRelExtension) import Wasp.Valid (ValidationError) import qualified Wasp.Valid as Valid @@ -78,7 +51,6 @@ analyzeWaspProject :: IO (Either [CompileError] AS.AppSpec, [CompileWarning]) analyzeWaspProject waspDir options = do waspFilePathOrError <- left (: []) <$> findWaspFile waspDir - case waspFilePathOrError of Left err -> return (Left err, []) Right waspFilePath -> @@ -93,116 +65,6 @@ analyzeWaspProject waspDir options = do Left errors -> return (Left errors, []) Right externalConfigs -> constructAppSpec waspDir options externalConfigs prismaSchemaAst declarations -data CompiledWaspJsFile - -data AppSpecDeclsJsonFile - -analyzeWaspFile :: Path' Abs (Dir WaspProjectDir) -> Psl.Schema.Schema -> WaspFilePath -> 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: 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. - compiledWaspJsFile <- ExceptT $ compileWaspTsFile waspProjectDir [relfile|tsconfig.wasp.json|] waspFilePath - declsJsonFile <- ExceptT $ executeMainWaspJsFileAndGetDeclsFile waspProjectDir prismaSchemaAst compiledWaspJsFile - ExceptT $ readDecls prismaSchemaAst declsJsonFile - -compileWaspTsFile :: - Path' Abs (Dir WaspProjectDir) -> - Path' (Rel WaspProjectDir) File' -> - Path' Abs (File WaspTsFile) -> - IO (Either [CompileError] (Path' Abs (File CompiledWaspJsFile))) -compileWaspTsFile waspProjectDir tsconfigNodeFileInWaspProjectDir waspFilePath = do - chan <- newChan - (_, tscExitCode) <- - concurrently - (readJobMessagesAndPrintThemPrefixed chan) - ( runNodeCommandAsJob - waspProjectDir - "npx" - [ "tsc", - "-p", - fromAbsFile (waspProjectDir tsconfigNodeFileInWaspProjectDir), - "--noEmit", - "false", - "--outDir", - fromAbsDir outDir - ] - J.Wasp - chan - ) - return $ case tscExitCode of - ExitFailure _status -> Left ["Got TypeScript compiler errors for " ++ fromAbsFile waspFilePath ++ "."] - ExitSuccess -> Right absCompiledWaspJsFile - where - outDir = waspProjectDir dotWaspDirInWaspProjectDir - absCompiledWaspJsFile = outDir compiledWaspJsFileInDotWaspDir - compiledWaspJsFileInDotWaspDir = castFile $ case replaceRelExtension (basename waspFilePath) ".js" of - Just path -> path - Nothing -> error $ "Couldn't calculate the compiled JS file path for " ++ fromAbsFile waspFilePath ++ "." - -executeMainWaspJsFileAndGetDeclsFile :: - Path' Abs (Dir WaspProjectDir) -> - Psl.Schema.Schema -> - Path' Abs (File CompiledWaspJsFile) -> - IO (Either [CompileError] (Path' Abs (File AppSpecDeclsJsonFile))) -executeMainWaspJsFileAndGetDeclsFile waspProjectDir prismaSchemaAst 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 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|] - [ "wasp-config", - fromAbsFile absCompiledMainWaspJsFile, - fromAbsFile absDeclsOutputFile, - encodeToString allowedEntityNames - ] - J.Wasp - chan - ) - case runExitCode of - ExitFailure _status -> return $ Left ["Error while running the compiled *.wasp.ts file."] - ExitSuccess -> return $ Right absDeclsOutputFile - where - absDeclsOutputFile = waspProjectDir dotWaspDirInWaspProjectDir [relfile|decls.json|] - allowedEntityNames = Psl.Schema.getModelNames prismaSchemaAst - -readDecls :: Psl.Schema.Schema -> Path' Abs (File AppSpecDeclsJsonFile) -> IO (Either [CompileError] [AS.Decl]) -readDecls prismaSchemaAst declsJsonFile = runExceptT $ do - entityDecls <- liftEither entityDeclsOrErrors - remainingDecls <- ExceptT $ left (: []) <$> declsFromJsonOrError - return $ entityDecls ++ remainingDecls - where - entityDeclsOrErrors = - left (map fst) $ - left (map getErrorMessageAndCtx) $ - Analyzer.getEntityDecls prismaSchemaAst - - declsFromJsonOrError = do - declsBytestring <- IOUtil.readFileBytes declsJsonFile - return $ - left ("Error while reading the declarations from JSON: " ++) $ - Aeson.eitherDecode declsBytestring - -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 - -analyzeWaspFileContent :: Psl.Schema.Schema -> String -> IO (Either [(String, Ctx)] [AS.Decl]) -analyzeWaspFileContent prismaSchemaAst = return . left (map getErrorMessageAndCtx) . Analyzer.analyze prismaSchemaAst - constructAppSpec :: Path' Abs (Dir WaspProjectDir) -> CompileOptions -> @@ -245,23 +107,6 @@ constructAppSpec waspDir options externalConfigs parsedPrismaSchema decls = do return $ runValidation ASV.validateAppSpec appSpec -findWaspFile :: Path' Abs (Dir WaspProjectDir) -> IO (Either String WaspFilePath) -findWaspFile waspDir = do - files <- fst <$> IOUtil.listDirectory waspDir - return $ case (findWaspTsFile files, findWaspLangFile files) of - (Just _, Just _) -> Left bothFilesFoundMessage - (Nothing, Nothing) -> Left fileNotFoundMessage - (Just waspTsFile, Nothing) -> Right waspTsFile - (Nothing, Just waspLangFile) -> Right waspLangFile - where - findWaspTsFile files = WaspTs <$> findFileThatEndsWith ".wasp.ts" files - findWaspLangFile files = WaspLang <$> findFileThatEndsWith ".wasp" files - findFileThatEndsWith suffix files = castFile . (waspDir ) <$> find ((suffix `isSuffixOf`) . fromRelFile) files - fileNotFoundMessage = "Couldn't find the *.wasp or a *.wasp.ts file in the " ++ fromAbsDir waspDir ++ " directory" - bothFilesFoundMessage = - "Found both *.wasp and *.wasp.ts files in the project directory. " - ++ "You must choose how you want to define your app (using Wasp or TypeScript) and only keep one of them." - analyzePrismaSchema :: Path' Abs (Dir WaspProjectDir) -> IO (Either [CompileError] Psl.Schema.Schema, [CompileWarning]) analyzePrismaSchema waspProjectDir = do findPrismaSchemaFile waspProjectDir >>= \case diff --git a/waspc/src/Wasp/Project/Common.hs b/waspc/src/Wasp/Project/Common.hs index 9fb166e924..662d261636 100644 --- a/waspc/src/Wasp/Project/Common.hs +++ b/waspc/src/Wasp/Project/Common.hs @@ -88,7 +88,6 @@ dotWaspInfoFileInGeneratedCodeDir = [relfile|.waspinfo|] packageJsonInWaspProjectDir :: Path' (Rel WaspProjectDir) (File PackageJsonFile) packageJsonInWaspProjectDir = [relfile|package.json|] --- TODO: The entire tsconfig story is very fragile getSrcTsConfigInWaspProjectDir :: WaspFilePath -> Path' (Rel WaspProjectDir) (File SrcTsConfigFile) getSrcTsConfigInWaspProjectDir = \case WaspTs _ -> srcTsConfigInWaspTsProject diff --git a/waspc/src/Wasp/Project/WaspFile.hs b/waspc/src/Wasp/Project/WaspFile.hs new file mode 100644 index 0000000000..82502ca07a --- /dev/null +++ b/waspc/src/Wasp/Project/WaspFile.hs @@ -0,0 +1,57 @@ +module Wasp.Project.WaspFile + ( findWaspFile, + analyzeWaspFile, + ) +where + +import Data.List (find, isSuffixOf) +import StrongPath + ( Abs, + Dir, + Path', + castFile, + fromAbsDir, + fromRelFile, + (), + ) +import qualified Wasp.AppSpec as AS +import Wasp.AppSpec.Core.Decl.JSON () +import Wasp.Project.Common + ( CompileError, + WaspFilePath (..), + WaspProjectDir, + ) +import Wasp.Project.WaspFile.TypeScript (analyzeWaspTsFile) +import Wasp.Project.WaspFile.WaspLang (analyzeWaspLangFile) +import qualified Wasp.Psl.Ast.Schema as Psl.Schema +import qualified Wasp.Util.IO as IOUtil + +findWaspFile :: Path' Abs (Dir WaspProjectDir) -> IO (Either String WaspFilePath) +findWaspFile waspDir = do + files <- fst <$> IOUtil.listDirectory waspDir + return $ case (findWaspTsFile files, findWaspLangFile files) of + (Just _, Just _) -> Left bothFilesFoundMessage + (Nothing, Nothing) -> Left fileNotFoundMessage + (Just waspTsFile, Nothing) -> Right waspTsFile + (Nothing, Just waspLangFile) -> Right waspLangFile + where + findWaspTsFile files = WaspTs <$> findFileThatEndsWith ".wasp.ts" files + findWaspLangFile files = WaspLang <$> findFileThatEndsWith ".wasp" files + findFileThatEndsWith suffix files = + castFile + . (waspDir ) + <$> find ((suffix `isSuffixOf`) . fromRelFile) files + + fileNotFoundMessage = "Couldn't find the *.wasp or a *.wasp.ts file in the " ++ fromAbsDir waspDir ++ " directory" + bothFilesFoundMessage = + "Found both *.wasp and *.wasp.ts files in the project directory. " + ++ "You must choose how you want to define your app (using Wasp or TypeScript) and only keep one of them." + +analyzeWaspFile :: + Path' Abs (Dir WaspProjectDir) -> + Psl.Schema.Schema -> + WaspFilePath -> + IO (Either [CompileError] [AS.Decl]) +analyzeWaspFile waspDir prismaSchemaAst = \case + WaspLang waspFilePath -> analyzeWaspLangFile prismaSchemaAst waspFilePath + WaspTs waspFilePath -> analyzeWaspTsFile waspDir prismaSchemaAst waspFilePath diff --git a/waspc/src/Wasp/Project/WaspFile/TypeScript.hs b/waspc/src/Wasp/Project/WaspFile/TypeScript.hs new file mode 100644 index 0000000000..e9508d4720 --- /dev/null +++ b/waspc/src/Wasp/Project/WaspFile/TypeScript.hs @@ -0,0 +1,166 @@ +module Wasp.Project.WaspFile.TypeScript + ( analyzeWaspTsFile, + ) +where + +import Control.Arrow (left) +import Control.Concurrent (newChan) +import Control.Concurrent.Async (concurrently) +import Control.Monad.Except (ExceptT (ExceptT), liftEither, runExceptT) +import qualified Data.Aeson as Aeson +import StrongPath + ( Abs, + Dir, + File, + File', + Path', + Rel, + basename, + castFile, + fromAbsDir, + fromAbsFile, + relfile, + (), + ) +import System.Exit (ExitCode (..)) +import qualified Wasp.Analyzer as Analyzer +import qualified Wasp.AppSpec as AS +import Wasp.AppSpec.Core.Decl.JSON () +import qualified Wasp.Job as J +import Wasp.Job.IO (readJobMessagesAndPrintThemPrefixed) +import Wasp.Job.Process (runNodeCommandAsJob) +import Wasp.Project.Common + ( CompileError, + WaspProjectDir, + WaspTsFile, + dotWaspDirInWaspProjectDir, + ) +import qualified Wasp.Psl.Ast.Model as Psl.Schema.Model +import qualified Wasp.Psl.Ast.Schema as Psl.Schema +import Wasp.Util (orElse) +import Wasp.Util.Aeson (encodeToString) +import qualified Wasp.Util.IO as IOUtil +import Wasp.Util.StrongPath (replaceRelExtension) + +data CompiledWaspJsFile + +data AppSpecDeclsJsonFile + +analyzeWaspTsFile :: + Path' Abs (Dir WaspProjectDir) -> + Psl.Schema.Schema -> + Path' Abs (File WaspTsFile) -> + IO (Either [CompileError] [AS.Decl]) +analyzeWaspTsFile waspProjectDir prismaSchemaAst waspFilePath = runExceptT $ do + -- TODO: I'm not yet sure where tsconfig.wasp.json location should come from + -- because we also need that knowledge when generating a TS SDK project. + compiledWaspJsFile <- ExceptT $ compileWaspTsFile waspProjectDir [relfile|tsconfig.wasp.json|] waspFilePath + declsJsonFile <- ExceptT $ executeMainWaspJsFileAndGetDeclsFile waspProjectDir prismaSchemaAst compiledWaspJsFile + ExceptT $ readDecls prismaSchemaAst declsJsonFile + +compileWaspTsFile :: + Path' Abs (Dir WaspProjectDir) -> + Path' (Rel WaspProjectDir) File' -> + Path' Abs (File WaspTsFile) -> + IO (Either [CompileError] (Path' Abs (File CompiledWaspJsFile))) +compileWaspTsFile waspProjectDir tsconfigNodeFileInWaspProjectDir waspFilePath = do + chan <- newChan + (_, tscExitCode) <- + concurrently + (readJobMessagesAndPrintThemPrefixed chan) + ( runNodeCommandAsJob + waspProjectDir + "npx" + -- We're using tsc to compile the *.wasp.ts file into a JS file. + -- + -- The tsconfig.wasp.json is configured to give our users with the + -- best possible IDE support while coding the *.wasp.ts file. + -- + -- When we actually want to compile the *.wasp.ts file, we must + -- override some of those rules. + -- + -- Tehnically, some overrides could have been specified + -- in the tsconfig.wasp.json file, but we decided to keep them here + -- because it helps users avoid accidentally breaking things. + [ "tsc", + "-p", + fromAbsFile (waspProjectDir tsconfigNodeFileInWaspProjectDir), + -- The tsconfig.wasp.json file has the noEmit flag on. + -- The file only exists IDE support, and we don't want users to + -- accidentally chage the outDir. + -- + -- Here, to actually generate the JS file in the desired location, + -- we must turn off the noEmit flag and specify the outDir. + "--noEmit", + "false", + "--outDir", + fromAbsDir outDir + ] + J.Wasp + chan + ) + return $ case tscExitCode of + ExitFailure _status -> Left ["Got TypeScript compiler errors for " ++ fromAbsFile waspFilePath ++ "."] + ExitSuccess -> Right absCompiledWaspJsFile + where + outDir = waspProjectDir dotWaspDirInWaspProjectDir + -- We know this will be the output JS file's location because it's how TSC + -- works (assuming we've specified the outDir, which we did). + absCompiledWaspJsFile = outDir compiledWaspJsFileInDotWaspDir + compiledWaspJsFileInDotWaspDir = + castFile $ + replaceRelExtension (basename waspFilePath) ".js" + `orElse` error ("Couldn't calculate the compiled JS file path for " ++ fromAbsFile waspFilePath ++ ".") + +executeMainWaspJsFileAndGetDeclsFile :: + Path' Abs (Dir WaspProjectDir) -> + Psl.Schema.Schema -> + Path' Abs (File CompiledWaspJsFile) -> + IO (Either [CompileError] (Path' Abs (File AppSpecDeclsJsonFile))) +executeMainWaspJsFileAndGetDeclsFile waspProjectDir prismaSchemaAst 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 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|] + [ "wasp-config", + fromAbsFile absCompiledMainWaspJsFile, + fromAbsFile absDeclsOutputFile, + -- When the user is coding main.wasp.ts, TypeScript must know about + -- all the available entities to warn the user if they use an + -- entity that doesn't exist. + encodeToString allowedEntityNames + ] + J.Wasp + chan + ) + case runExitCode of + ExitFailure _status -> return $ Left ["Error while running the compiled *.wasp.ts file."] + ExitSuccess -> return $ Right absDeclsOutputFile + where + absDeclsOutputFile = waspProjectDir dotWaspDirInWaspProjectDir [relfile|decls.json|] + allowedEntityNames = Psl.Schema.Model.getName <$> Psl.Schema.getModels prismaSchemaAst + +readDecls :: Psl.Schema.Schema -> Path' Abs (File AppSpecDeclsJsonFile) -> IO (Either [CompileError] [AS.Decl]) +readDecls prismaSchemaAst declsJsonFile = runExceptT $ do + entityDecls <- liftEither entityDeclsOrErrors + remainingDecls <- ExceptT $ left (: []) <$> declsFromJsonOrError + return $ entityDecls ++ remainingDecls + where + entityDeclsOrErrors = + left (map fst) $ + left (map Analyzer.getErrorMessageAndCtx) $ + Analyzer.getEntityDecls prismaSchemaAst + + declsFromJsonOrError = do + declsBytestring <- IOUtil.readFileBytes declsJsonFile + return $ + left ("Error while reading the declarations from JSON: " ++) $ + Aeson.eitherDecode declsBytestring diff --git a/waspc/src/Wasp/Project/WaspFile/WaspLang.hs b/waspc/src/Wasp/Project/WaspFile/WaspLang.hs new file mode 100644 index 0000000000..065596d37a --- /dev/null +++ b/waspc/src/Wasp/Project/WaspFile/WaspLang.hs @@ -0,0 +1,35 @@ +module Wasp.Project.WaspFile.WaspLang + ( analyzeWaspLangFile, + analyzeWaspFileContent, + ) +where + +import Control.Arrow (left) +import StrongPath + ( Abs, + File, + Path', + ) +import qualified Wasp.Analyzer as Analyzer +import Wasp.Analyzer.Parser.Ctx (Ctx) +import qualified Wasp.AppSpec as AS +import Wasp.AppSpec.Core.Decl.JSON () +import Wasp.Error (showCompilerErrorForTerminal) +import Wasp.Project.Common + ( CompileError, + WaspLangFile, + ) +import qualified Wasp.Psl.Ast.Schema as Psl.Schema +import qualified Wasp.Util.IO as IOUtil + +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 + +analyzeWaspFileContent :: Psl.Schema.Schema -> String -> IO (Either [(String, Ctx)] [AS.Decl]) +analyzeWaspFileContent prismaSchemaAst = + return + . left (map Analyzer.getErrorMessageAndCtx) + . Analyzer.analyze prismaSchemaAst diff --git a/waspc/src/Wasp/Psl/Ast/Schema.hs b/waspc/src/Wasp/Psl/Ast/Schema.hs index 403b4452dc..df551c9ea8 100644 --- a/waspc/src/Wasp/Psl/Ast/Schema.hs +++ b/waspc/src/Wasp/Psl/Ast/Schema.hs @@ -7,7 +7,6 @@ module Wasp.Psl.Ast.Schema getEnums, getDatasources, getGenerators, - getModelNames, ) where @@ -15,7 +14,6 @@ import Wasp.Psl.Ast.ConfigBlock (ConfigBlock) import qualified Wasp.Psl.Ast.ConfigBlock as Psl.ConfigBlock import Wasp.Psl.Ast.Enum (Enum) import Wasp.Psl.Ast.Model (Model) -import qualified Wasp.Psl.Ast.Model as Model import Wasp.Psl.Ast.Type (Type) import Wasp.Psl.Ast.View (View) import Prelude hiding (Enum) @@ -51,6 +49,3 @@ getGenerators schema = [generator | generator@((Psl.ConfigBlock.ConfigBlock Psl. getConfigBlocks :: Schema -> [ConfigBlock] getConfigBlocks (Schema blocks) = [configBlock | ConfigBlock configBlock <- blocks] - -getModelNames :: Schema -> [String] -getModelNames schema = map Model.getName $ getModels schema diff --git a/waspc/src/Wasp/Util.hs b/waspc/src/Wasp/Util.hs index 9cedb4e0d7..8be4da55f1 100644 --- a/waspc/src/Wasp/Util.hs +++ b/waspc/src/Wasp/Util.hs @@ -34,6 +34,7 @@ module Wasp.Util kebabToCamelCase, maybeToEither, eitherToMaybe, + orElse, whenM, naiveTrimJSON, textToLazyBS, @@ -250,6 +251,9 @@ eitherToMaybe :: Either e a -> Maybe a eitherToMaybe (Right x) = Just x eitherToMaybe (Left _) = Nothing +orElse :: Maybe a -> a -> a +orElse = flip fromMaybe + getEnvVarDefinition :: (String, String) -> String getEnvVarDefinition (name, value) = concat [name, "=", value] diff --git a/waspc/tools/install_packages_to_data_dir.sh b/waspc/tools/install_packages_to_data_dir.sh index 730ce94138..67acdcb579 100755 --- a/waspc/tools/install_packages_to_data_dir.sh +++ b/waspc/tools/install_packages_to_data_dir.sh @@ -9,6 +9,9 @@ dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) for package in $(ls "$dir/../packages"); do package_dir="$dir/../packages/$package" if [[ -d "$package_dir" ]]; then + # We're only installing the dependencies here to verify that the build + # works, that's why the node_modules folder is removed immediately after. + # The real dependency installation happens in Haskell. echo "Installing $package ($package_dir)" cd "$package_dir" npm install diff --git a/waspc/waspc.cabal b/waspc/waspc.cabal index e7fdd2317e..f428c957a8 100644 --- a/waspc/waspc.cabal +++ b/waspc/waspc.cabal @@ -293,11 +293,6 @@ library Wasp.Generator.FileDraft.CopyAndModifyTextFileDraft Wasp.Generator.FileDraft.Writeable Wasp.Generator.FileDraft.WriteableMonad - Wasp.Generator.Job - Wasp.Generator.Job.Common - Wasp.Generator.Job.IO - Wasp.Generator.Job.IO.PrefixedWriter - Wasp.Generator.Job.Process Wasp.Generator.JsImport Wasp.Generator.Monad Wasp.Generator.NpmDependencies @@ -357,6 +352,11 @@ library Wasp.Generator.WebSocket Wasp.Generator.WriteFileDrafts Wasp.JsImport + Wasp.Job + Wasp.Job.Common + Wasp.Job.IO + Wasp.Job.IO.PrefixedWriter + Wasp.Job.Process Wasp.Message Wasp.Node.Version Wasp.NpmDependency @@ -365,10 +365,10 @@ library Wasp.Project.Analyze Wasp.Project.Common Wasp.Project.Db - Wasp.Project.Db.Migrations Wasp.Project.Db.Dev Wasp.Project.Db.Dev.Postgres Wasp.Project.Db.Dev.Sqlite + Wasp.Project.Db.Migrations Wasp.Project.Deployment Wasp.Project.Env Wasp.Project.ExternalConfig @@ -377,6 +377,9 @@ library Wasp.Project.ExternalFiles Wasp.Project.Studio Wasp.Project.Vite + Wasp.Project.WaspFile + Wasp.Project.WaspFile.TypeScript + Wasp.Project.WaspFile.WaspLang Wasp.Psl.Ast.Argument Wasp.Psl.Ast.Attribute Wasp.Psl.Ast.Common diff --git a/waspc/waspls/src/Wasp/LSP/Prisma/Analyze.hs b/waspc/waspls/src/Wasp/LSP/Prisma/Analyze.hs index 0ec4b7565d..49499cf538 100644 --- a/waspc/waspls/src/Wasp/LSP/Prisma/Analyze.hs +++ b/waspc/waspls/src/Wasp/LSP/Prisma/Analyze.hs @@ -8,7 +8,8 @@ import Wasp.LSP.ServerMonads (ServerM, modify) import qualified Wasp.LSP.ServerState as State import Wasp.Project (WaspProjectDir) import Wasp.Project.Analyze (analyzePrismaSchema) -import Wasp.Psl.Ast.Schema (getModelNames) +import qualified Wasp.Psl.Ast.Model as Model +import Wasp.Psl.Ast.Schema (getModels) analyzeAndSetPrismaSchema :: Path' Abs (Dir WaspProjectDir) -> ServerM () analyzeAndSetPrismaSchema waspDir = do @@ -18,7 +19,7 @@ analyzeAndSetPrismaSchema waspDir = do logOutput "warnings" $ show warnings (Right prismaSchemaAst, warnings) -> do logOutput "warnings" $ show warnings - logOutput "models" $ show $ getModelNames prismaSchemaAst + logOutput "models" $ show $ Model.getName <$> getModels prismaSchemaAst modify (State.prismaSchemaAst .~ prismaSchemaAst) where logOutput :: String -> String -> ServerM ()