Skip to content

Commit

Permalink
Clean up TS config technical debt (#2435)
Browse files Browse the repository at this point in the history
  • Loading branch information
sodic authored Jan 2, 2025
1 parent a7ad43a commit 4c9e7e3
Show file tree
Hide file tree
Showing 42 changed files with 631 additions and 445 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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 ()
Expand Down Expand Up @@ -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 ()
Expand Down
2 changes: 1 addition & 1 deletion waspc/cli/src/Wasp/Cli/Command/Db/Studio.hs
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
6 changes: 3 additions & 3 deletions waspc/cli/src/Wasp/Cli/Command/TsConfigSetup.hs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
20 changes: 10 additions & 10 deletions waspc/packages/wasp-config/eslint.config.js
Original file line number Diff line number Diff line change
@@ -1,26 +1,26 @@
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,
},
},
// 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',
},
},
];
]
189 changes: 118 additions & 71 deletions waspc/packages/wasp-config/src/appSpec.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,41 @@
/** 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<T extends Decl['declType']> = Extract<
Decl,
{ declType: T }
>

// NOTE: Entities are defined in the schema.prisma file, but they can still be
// referenced.
export type DeclType = Decl['declType'] | 'Entity'

export type Page = {
component: ExtImport
authRequired?: boolean
authRequired: Optional<boolean>
}

export type Route = {
Expand All @@ -32,39 +45,39 @@ export type Route = {

export type Action = {
fn: ExtImport
entities?: Ref<'Entity'>[]
auth?: boolean
entities: Optional<Ref<'Entity'>[]>
auth: Optional<boolean>
}

export type Query = {
fn: ExtImport
entities?: Ref<'Entity'>[]
auth?: boolean
entities: Optional<Ref<'Entity'>[]>
auth: Optional<boolean>
}

export type Job = {
executor: JobExecutor
perform: Perform
schedule?: Schedule
entities?: Ref<'Entity'>[]
schedule: Optional<Schedule>
entities: Optional<Ref<'Entity'>[]>
}
export type Schedule = {
cron: string
args?: object
executorOptions?: ExecutorOptions
args: Optional<object>
executorOptions: Optional<ExecutorOptions>
}

export type Perform = {
fn: ExtImport
executorOptions?: ExecutorOptions
executorOptions: Optional<ExecutorOptions>
}

export type Api = {
fn: ExtImport
middlewareConfigFn?: ExtImport
entities?: Ref<'Entity'>[]
middlewareConfigFn: Optional<ExtImport>
entities: Optional<Ref<'Entity'>[]>
httpRoute: HttpRoute
auth?: boolean
auth: Optional<boolean>
}

export type ApiNamespace = {
Expand All @@ -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<string[]>
auth: Optional<Auth>
server: Optional<Server>
client: Optional<Client>
db: Optional<Db>
emailSender: Optional<EmailSender>
webSocket: Optional<WebSocket>
}

export type ExtImport = {
Expand All @@ -98,89 +111,87 @@ export type ExtImport = {
export type JobExecutor = 'PgBoss'

export type ExecutorOptions = {
pgBoss?: object
pgBoss: Optional<object>
}

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<CrudOperationOptions>
getAll: Optional<CrudOperationOptions>
create: Optional<CrudOperationOptions>
update: Optional<CrudOperationOptions>
delete: Optional<CrudOperationOptions>
}

export type CrudOperationOptions = {
isPublic?: boolean
overrideFn?: ExtImport
isPublic: Optional<boolean>
overrideFn: Optional<ExtImport>
}

export type Wasp = {
// TODO: Check semver in export type system?
version: string
}

export type Auth = {
userEntity: Ref<'Entity'>
externalAuthEntity?: Ref<'Entity'>
externalAuthEntity: Optional<Ref<'Entity'>>
methods: AuthMethods
onAuthFailedRedirectTo: string
onAuthSucceededRedirectTo?: string
onBeforeSignup?: ExtImport
onAfterSignup?: ExtImport
onBeforeOAuthRedirect?: ExtImport
onBeforeLogin?: ExtImport
onAfterLogin?: ExtImport
onAuthSucceededRedirectTo: Optional<string>
onBeforeSignup: Optional<ExtImport>
onAfterSignup: Optional<ExtImport>
onBeforeOAuthRedirect: Optional<ExtImport>
onBeforeLogin: Optional<ExtImport>
onAfterLogin: Optional<ExtImport>
}

export type AuthMethods = {
usernameAndPassword?: UsernameAndPasswordConfig
discord?: ExternalAuthConfig
google?: ExternalAuthConfig
gitHub?: ExternalAuthConfig
keycloak?: ExternalAuthConfig
email?: EmailAuthConfig
usernameAndPassword: Optional<UsernameAndPasswordConfig>
discord: Optional<ExternalAuthConfig>
google: Optional<ExternalAuthConfig>
gitHub: Optional<ExternalAuthConfig>
keycloak: Optional<ExternalAuthConfig>
email: Optional<EmailAuthConfig>
}

export type UsernameAndPasswordConfig = {
userSignupFields?: ExtImport
userSignupFields: Optional<ExtImport>
}

export type ExternalAuthConfig = {
configFn?: ExtImport
userSignupFields?: ExtImport
configFn: Optional<ExtImport>
userSignupFields: Optional<ExtImport>
}

export type EmailAuthConfig = {
userSignupFields?: ExtImport
userSignupFields: Optional<ExtImport>
fromField: EmailFromField
emailVerification: EmailVerificationConfig
passwordReset: PasswordResetConfig
}

export type EmailSender = {
provider: EmailProvider
defaultFrom?: EmailFromField
defaultFrom: Optional<EmailFromField>
}

// TODO: duplication
export type EmailProvider = 'SMTP' | 'SendGrid' | 'Mailgun' | 'Dummy'

export type EmailFromField = {
name?: string
name: Optional<string>
email: string
}

export type EmailVerificationConfig = {
getEmailContentFn?: ExtImport
getEmailContentFn: Optional<ExtImport>
clientRoute: Ref<'Route'>
}

export type PasswordResetConfig = {
getEmailContentFn?: ExtImport
getEmailContentFn: Optional<ExtImport>
clientRoute: Ref<'Route'>
}

Expand All @@ -190,21 +201,57 @@ export type Ref<T extends DeclType> = {
}

export type Server = {
setupFn?: ExtImport
middlewareConfigFn?: ExtImport
setupFn: Optional<ExtImport>
middlewareConfigFn: Optional<ExtImport>
}

export type Client = {
setupFn?: ExtImport
rootComponent?: ExtImport
baseDir?: `/${string}`
setupFn: Optional<ExtImport>
rootComponent: Optional<ExtImport>
baseDir: Optional<`/${string}`>
}

export type Db = {
seeds?: ExtImport[]
seeds: Optional<ExtImport[]>
}

export type WebSocket = {
fn: ExtImport
autoConnect?: boolean
autoConnect: Optional<boolean>
}

/**
* 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<string>`), 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> = T | undefined
Loading

0 comments on commit 4c9e7e3

Please sign in to comment.