From e92cb252490ef3ce318ae2ee97a06a4029886aa6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Sodi=C4=87?= Date: Thu, 10 Oct 2024 11:00:35 +0200 Subject: [PATCH] Support defining the app in TypeScript (TS SDK) (#2335) Implement the TS SDK as a preview feature. --- waspc/ChangeLog.md | 71 +- waspc/cli/exe/Main.hs | 5 +- waspc/cli/src/Wasp/Cli/Command/Call.hs | 1 + waspc/cli/src/Wasp/Cli/Command/Common.hs | 2 +- .../StarterTemplates/Templating.hs | 28 +- .../cli/src/Wasp/Cli/Command/TsConfigSetup.hs | 38 + waspc/data/Cli/templates/basic/package.json | 1 + .../waspBuild/.wasp/build/package.json | 3 +- .../waspBuild-golden/waspBuild/package.json | 3 +- .../waspCompile/package.json | 3 +- .../waspComplexTest/package.json | 3 +- .../waspJob-golden/waspJob/package.json | 3 +- .../waspMigrate/package.json | 3 +- .../waspNew-golden/waspNew/package.json | 3 +- waspc/packages/wasp-config/.gitignore | 4 + waspc/packages/wasp-config/.prettierrc.json | 6 + waspc/packages/wasp-config/eslint.config.js | 26 + waspc/packages/wasp-config/nodemon.json | 6 + waspc/packages/wasp-config/package-lock.json | 1870 +++++++++++++++++ waspc/packages/wasp-config/package.json | 27 + waspc/packages/wasp-config/src/_private.ts | 1 + waspc/packages/wasp-config/src/appSpec.ts | 210 ++ waspc/packages/wasp-config/src/index.ts | 1 + waspc/packages/wasp-config/src/mappers.ts | 449 ++++ waspc/packages/wasp-config/src/run.ts | 90 + waspc/packages/wasp-config/src/userApi.ts | 281 +++ waspc/packages/wasp-config/tsconfig.json | 25 + waspc/src/Wasp/Analyzer.hs | 20 +- .../Evaluation/TypedExpr/Combinators.hs | 23 +- waspc/src/Wasp/Analyzer/Prisma.hs | 6 +- waspc/src/Wasp/AppSpec/App/Wasp.hs | 3 - waspc/src/Wasp/AppSpec/ExtImport.hs | 34 +- waspc/src/Wasp/Error.hs | 5 +- .../Wasp/Generator/ExternalConfig/TsConfig.hs | 6 +- waspc/src/Wasp/Generator/NpmInstall.hs | 3 +- waspc/src/Wasp/NodePackageFFI.hs | 41 +- waspc/src/Wasp/Project/Analyze.hs | 166 +- waspc/src/Wasp/Project/Common.hs | 40 +- waspc/src/Wasp/Project/Deployment.hs | 2 +- waspc/src/Wasp/Project/ExternalConfig.hs | 14 +- .../Wasp/Project/ExternalConfig/TsConfig.hs | 39 +- waspc/src/Wasp/Project/Studio.hs | 2 +- waspc/src/Wasp/Psl/Ast/Model.hs | 6 +- waspc/src/Wasp/Psl/Ast/Schema.hs | 5 + waspc/src/Wasp/TypeScript/Inspect/Exports.hs | 2 +- waspc/src/Wasp/Util/Aeson.hs | 6 +- waspc/test/AppSpec/FromJSONTest.hs | 30 +- waspc/waspc.cabal | 2 +- .../src/Wasp/LSP/ExtImport/ExportsCache.hs | 4 +- waspc/waspls/src/Wasp/LSP/Prisma/Analyze.hs | 4 +- waspc/waspls/src/Wasp/LSP/Prisma/Util.hs | 13 - 51 files changed, 3475 insertions(+), 164 deletions(-) create mode 100644 waspc/cli/src/Wasp/Cli/Command/TsConfigSetup.hs create mode 100644 waspc/packages/wasp-config/.gitignore create mode 100644 waspc/packages/wasp-config/.prettierrc.json create mode 100644 waspc/packages/wasp-config/eslint.config.js create mode 100644 waspc/packages/wasp-config/nodemon.json create mode 100644 waspc/packages/wasp-config/package-lock.json create mode 100644 waspc/packages/wasp-config/package.json create mode 100644 waspc/packages/wasp-config/src/_private.ts create mode 100644 waspc/packages/wasp-config/src/appSpec.ts create mode 100644 waspc/packages/wasp-config/src/index.ts create mode 100644 waspc/packages/wasp-config/src/mappers.ts create mode 100644 waspc/packages/wasp-config/src/run.ts create mode 100644 waspc/packages/wasp-config/src/userApi.ts create mode 100644 waspc/packages/wasp-config/tsconfig.json delete mode 100644 waspc/waspls/src/Wasp/LSP/Prisma/Util.hs diff --git a/waspc/ChangeLog.md b/waspc/ChangeLog.md index 7cc4ab7cf6..6e426aa19a 100644 --- a/waspc/ChangeLog.md +++ b/waspc/ChangeLog.md @@ -2,16 +2,77 @@ ## 0.15.0 -### 🎉 New Features +### 🎉 New Features and improvements -- Upgrade to the latest Prisma version which makes Wasp faster! -- Upgrade to the latest React Router version which sets us up for some cool new features in the future. +#### Write your app config in TypeScript (preview feature) + +Wasp 0.15.0 ships a preview feature that lets you define your app using TypeScript instead of the Wasp language. + +So, instead of this: + +```c +app TodoApp { + wasp: { + version: "^0.15.0" + }, + title: "TodoApp", + auth: { + userEntity: User, + methods: { + usernameAndPassword: {} + }, + onAuthFailedRedirectTo: "/login" + } +} + +route RootRoute { path: "/", to: MainPage } +page MainPage { + authRequired: true, + component: import { MainPage } from "@src/MainPage" +} +``` + +You can now write this: + +```typescript + +improt { App } from 'wasp-config' + +const app = new App('TodoApp', { + title: 'TodoApp', + wasp: { + version: '^0.15.0', + }, +}) + +app.auth({ + userEntity: 'User', + methods: { + usernameAndPassword: {} + }, + onAuthFailedRedirectTo: '/login', +}) + +const mainPage = app.page('MainPage', { + authRequired: true, + component: { import: 'MainPage', from: '@src/MainPage' }, +}) + +app.route('RootRoute', { + path: '/', + to: mainPage, +}) +``` + +To learn more about this feature and how to activate it, check out the docs. ### ⚠️ Breaking Changes There are some breaking changes with React Router 6 which will require you to update your code. +Also, the new version of Prisma may cause breaking changes depending on how you're using it. + -Read more about them in the migration guide: https://wasp-lang.dev/docs/migration-guides/migrate-from-0-14-to-0-15 +Read more about breaking changes in the migration guide: https://wasp-lang.dev/docs/migration-guides/migrate-from-0-14-to-0-15 ### 🐞 Bug fixes @@ -20,6 +81,8 @@ Read more about them in the migration guide: https://wasp-lang.dev/docs/migratio ### 🔧 Small improvements +- Upgrade to the latest Prisma version which makes Wasp faster! +- Upgrade to the latest React Router version which sets us up for some cool new features in the future. - Enable users to use Mailgun's EU region by setting the `MAILGUN_API_URL` env variable. - Validate `userEntity` ID field's `@default` attribute. diff --git a/waspc/cli/exe/Main.hs b/waspc/cli/exe/Main.hs index 961a12a735..c68e30ebda 100644 --- a/waspc/cli/exe/Main.hs +++ b/waspc/cli/exe/Main.hs @@ -32,6 +32,7 @@ import qualified Wasp.Cli.Command.Start.Db as Command.Start.Db import Wasp.Cli.Command.Studio (studio) import qualified Wasp.Cli.Command.Telemetry as Telemetry import Wasp.Cli.Command.Test (test) +import Wasp.Cli.Command.TsConfigSetup (tsConfigSetup) import Wasp.Cli.Command.Uninstall (uninstall) import Wasp.Cli.Command.WaspLS (runWaspLS) import Wasp.Cli.Message (cliSendMessage) @@ -51,6 +52,7 @@ main = withUtf8 . (`E.catch` handleInternalErrors) $ do ["start"] -> Command.Call.Start ["start", "db"] -> Command.Call.StartDb ["clean"] -> Command.Call.Clean + ["ts-setup"] -> Command.Call.TsSetup ["compile"] -> Command.Call.Compile ("db" : dbArgs) -> Command.Call.Db dbArgs ["uninstall"] -> Command.Call.Uninstall @@ -102,6 +104,7 @@ main = withUtf8 . (`E.catch` handleInternalErrors) $ do Command.Call.Start -> runCommand start Command.Call.StartDb -> runCommand Command.Start.Db.start Command.Call.Clean -> runCommand clean + Command.Call.TsSetup -> runCommand tsConfigSetup Command.Call.Compile -> runCommand compile Command.Call.Db dbArgs -> dbCli dbArgs Command.Call.Version -> printVersion @@ -130,7 +133,7 @@ main = withUtf8 . (`E.catch` handleInternalErrors) $ do handleInternalErrors :: E.ErrorCall -> IO () handleInternalErrors e = do - putStrLn $ "\nInternal Wasp error (bug in compiler):\n" ++ indent 2 (show e) + putStrLn $ "\nInternal Wasp error (bug in the compiler):\n" ++ indent 2 (show e) exitFailure -- | Sets env variables that are visible to the commands run by the CLI. diff --git a/waspc/cli/src/Wasp/Cli/Command/Call.hs b/waspc/cli/src/Wasp/Cli/Command/Call.hs index beb665a5b6..1105322548 100644 --- a/waspc/cli/src/Wasp/Cli/Command/Call.hs +++ b/waspc/cli/src/Wasp/Cli/Command/Call.hs @@ -7,6 +7,7 @@ data Call | StartDb | Clean | Uninstall + | TsSetup | Compile | Db Arguments -- db args | Build diff --git a/waspc/cli/src/Wasp/Cli/Command/Common.hs b/waspc/cli/src/Wasp/Cli/Command/Common.hs index df5c396114..21fae7622b 100644 --- a/waspc/cli/src/Wasp/Cli/Command/Common.hs +++ b/waspc/cli/src/Wasp/Cli/Command/Common.hs @@ -5,8 +5,8 @@ module Wasp.Cli.Command.Common ) where -import Control.Monad.Except import qualified Control.Monad.Except as E +import Control.Monad.IO.Class (liftIO) import StrongPath (Abs, Dir, Path') import qualified StrongPath as SP import StrongPath.Operations 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 7c3cb58570..0dc791d2bd 100644 --- a/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates/Templating.hs +++ b/waspc/cli/src/Wasp/Cli/Command/CreateNewProject/StarterTemplates/Templating.hs @@ -9,7 +9,8 @@ import qualified Data.Text as T import StrongPath (Abs, Dir, File, Path') import Wasp.Cli.Command.CreateNewProject.Common (defaultWaspVersionBounds) import Wasp.Cli.Command.CreateNewProject.ProjectDescription (NewProjectAppName, NewProjectName) -import Wasp.Project.Analyze (findWaspFile) +import Wasp.NodePackageFFI (InstallablePackage (WaspConfigPackage), getPackageInstallationPath) +import Wasp.Project.Analyze (WaspFilePath (..), findWaspFile) import Wasp.Project.Common (WaspProjectDir) import Wasp.Project.ExternalConfig.PackageJson (findPackageJsonFile) import qualified Wasp.Util.IO as IOUtil @@ -26,8 +27,11 @@ replaceTemplatePlaceholdersInWaspFile :: NewProjectAppName -> NewProjectName -> Path' Abs (Dir WaspProjectDir) -> IO () replaceTemplatePlaceholdersInWaspFile appName projectName projectDir = findWaspFile projectDir >>= \case - Nothing -> return () - Just absMainWaspFile -> replaceTemplatePlaceholdersInFileOnDisk appName projectName absMainWaspFile + Left _error -> return () + Right (WaspLang absMainWaspFile) -> replaceTemplatePlaceholders absMainWaspFile + Right (WaspTs absMainTsFile) -> replaceTemplatePlaceholders absMainTsFile + where + replaceTemplatePlaceholders = replaceTemplatePlaceholdersInFileOnDisk appName projectName -- | Template file for package.json file has placeholders in it that we want to replace -- in the package.json file we have written to the disk. @@ -40,8 +44,16 @@ replaceTemplatePlaceholdersInPackageJsonFile appName projectName projectDir = Just absPackageJsonFile -> replaceTemplatePlaceholdersInFileOnDisk appName projectName absPackageJsonFile replaceTemplatePlaceholdersInFileOnDisk :: NewProjectAppName -> NewProjectName -> Path' Abs (File f) -> IO () -replaceTemplatePlaceholdersInFileOnDisk appName projectName = - updateFileContentWith (replacePlaceholders waspTemplateReplacements) +replaceTemplatePlaceholdersInFileOnDisk appName projectName file = do + waspConfigPackagePath <- getPackageInstallationPath WaspConfigPackage + let waspTemplateReplacements = + [ ("__waspConfigPath__", waspConfigPackagePath), + ("__waspAppName__", show appName), + ("__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 () updateFileContentWith updateFn absFilePath = IOUtil.readFileStrict absFilePath >>= IOUtil.writeFileFromText absFilePath . updateFn @@ -50,9 +62,3 @@ replaceTemplatePlaceholdersInFileOnDisk appName projectName = replacePlaceholders replacements content = foldl' replacePlaceholder content replacements where replacePlaceholder content' (placeholder, value) = T.replace (T.pack placeholder) (T.pack value) content' - - waspTemplateReplacements = - [ ("__waspAppName__", show appName), - ("__waspProjectName__", show projectName), - ("__waspVersion__", defaultWaspVersionBounds) - ] diff --git a/waspc/cli/src/Wasp/Cli/Command/TsConfigSetup.hs b/waspc/cli/src/Wasp/Cli/Command/TsConfigSetup.hs new file mode 100644 index 0000000000..beaf6328b8 --- /dev/null +++ b/waspc/cli/src/Wasp/Cli/Command/TsConfigSetup.hs @@ -0,0 +1,38 @@ +module Wasp.Cli.Command.TsConfigSetup (tsConfigSetup) where + +import Control.Concurrent (Chan, newChan) +import Control.Concurrent.Async (concurrently) +import Control.Monad.Except (throwError) +import Control.Monad.IO.Class (liftIO) +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 Wasp.NodePackageFFI (InstallablePackage (WaspConfigPackage), getPackageInstallationPath) + +-- | Prepares the project for using Wasp's TypeScript SDK. +tsConfigSetup :: Command () +tsConfigSetup = do + InWaspProject waspProjectDir <- require + messageChan <- liftIO newChan + -- NOTE: We're also installing the user's package.json dependencies here + -- This is to provide proper IDE support for users working with the TS SDK + -- (it needs the `wasp-config` package). + liftIO (installWaspConfigPackage messageChan waspProjectDir) + >>= onLeftThrowError + where + onLeftThrowError = either (throwError . CommandError "npm install failed") pure + +installWaspConfigPackage :: Chan J.JobMessage -> Path' Abs (Dir a) -> IO (Either String ()) +installWaspConfigPackage chan projectDir = do + installationPath <- getPackageInstallationPath WaspConfigPackage + (_, exitCode) <- + concurrently + (readJobMessagesAndPrintThemPrefixed chan) + (runNodeCommandAsJob projectDir "npm" ["install", "--save-dev", "file:" ++ installationPath] J.Wasp chan) + return $ case exitCode of + ExitSuccess -> Right () + ExitFailure _ -> Left "Failed to install wasp-config package" diff --git a/waspc/data/Cli/templates/basic/package.json b/waspc/data/Cli/templates/basic/package.json index d533706c1a..47102a526e 100644 --- a/waspc/data/Cli/templates/basic/package.json +++ b/waspc/data/Cli/templates/basic/package.json @@ -1,5 +1,6 @@ { "name": "__waspAppName__", + "type": "module", "dependencies": { "wasp": "file:.wasp/out/sdk/wasp", "react": "^18.2.0" diff --git a/waspc/e2e-test/test-outputs/waspBuild-golden/waspBuild/.wasp/build/package.json b/waspc/e2e-test/test-outputs/waspBuild-golden/waspBuild/.wasp/build/package.json index a4557082c4..7b5eed4125 100644 --- a/waspc/e2e-test/test-outputs/waspBuild-golden/waspBuild/.wasp/build/package.json +++ b/waspc/e2e-test/test-outputs/waspBuild-golden/waspBuild/.wasp/build/package.json @@ -9,5 +9,6 @@ "typescript": "^5.1.0", "vite": "^4.3.9" }, - "name": "waspBuild" + "name": "waspBuild", + "type": "module" } diff --git a/waspc/e2e-test/test-outputs/waspBuild-golden/waspBuild/package.json b/waspc/e2e-test/test-outputs/waspBuild-golden/waspBuild/package.json index a4557082c4..7b5eed4125 100644 --- a/waspc/e2e-test/test-outputs/waspBuild-golden/waspBuild/package.json +++ b/waspc/e2e-test/test-outputs/waspBuild-golden/waspBuild/package.json @@ -9,5 +9,6 @@ "typescript": "^5.1.0", "vite": "^4.3.9" }, - "name": "waspBuild" + "name": "waspBuild", + "type": "module" } diff --git a/waspc/e2e-test/test-outputs/waspCompile-golden/waspCompile/package.json b/waspc/e2e-test/test-outputs/waspCompile-golden/waspCompile/package.json index cae5e83e8d..1ffaf97439 100644 --- a/waspc/e2e-test/test-outputs/waspCompile-golden/waspCompile/package.json +++ b/waspc/e2e-test/test-outputs/waspCompile-golden/waspCompile/package.json @@ -9,5 +9,6 @@ "typescript": "^5.1.0", "vite": "^4.3.9" }, - "name": "waspCompile" + "name": "waspCompile", + "type": "module" } diff --git a/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/package.json b/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/package.json index d659437d0d..61062ad976 100644 --- a/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/package.json +++ b/waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/package.json @@ -11,5 +11,6 @@ }, "name": "waspComplexTest", "react-redux": "^7.1.3", - "redux": "^4.0.5" + "redux": "^4.0.5", + "type": "module" } diff --git a/waspc/e2e-test/test-outputs/waspJob-golden/waspJob/package.json b/waspc/e2e-test/test-outputs/waspJob-golden/waspJob/package.json index 9644c5e9d1..deb4a4baca 100644 --- a/waspc/e2e-test/test-outputs/waspJob-golden/waspJob/package.json +++ b/waspc/e2e-test/test-outputs/waspJob-golden/waspJob/package.json @@ -9,5 +9,6 @@ "typescript": "^5.1.0", "vite": "^4.3.9" }, - "name": "waspJob" + "name": "waspJob", + "type": "module" } diff --git a/waspc/e2e-test/test-outputs/waspMigrate-golden/waspMigrate/package.json b/waspc/e2e-test/test-outputs/waspMigrate-golden/waspMigrate/package.json index 987acc7d89..98da5e7388 100644 --- a/waspc/e2e-test/test-outputs/waspMigrate-golden/waspMigrate/package.json +++ b/waspc/e2e-test/test-outputs/waspMigrate-golden/waspMigrate/package.json @@ -9,5 +9,6 @@ "typescript": "^5.1.0", "vite": "^4.3.9" }, - "name": "waspMigrate" + "name": "waspMigrate", + "type": "module" } diff --git a/waspc/e2e-test/test-outputs/waspNew-golden/waspNew/package.json b/waspc/e2e-test/test-outputs/waspNew-golden/waspNew/package.json index 52321cde8e..5a4bb9c6ee 100644 --- a/waspc/e2e-test/test-outputs/waspNew-golden/waspNew/package.json +++ b/waspc/e2e-test/test-outputs/waspNew-golden/waspNew/package.json @@ -9,5 +9,6 @@ "typescript": "^5.1.0", "vite": "^4.3.9" }, - "name": "waspNew" + "name": "waspNew", + "type": "module" } diff --git a/waspc/packages/wasp-config/.gitignore b/waspc/packages/wasp-config/.gitignore new file mode 100644 index 0000000000..25879bed81 --- /dev/null +++ b/waspc/packages/wasp-config/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +dist/ +.env +.vscode/ diff --git a/waspc/packages/wasp-config/.prettierrc.json b/waspc/packages/wasp-config/.prettierrc.json new file mode 100644 index 0000000000..fa51da29e7 --- /dev/null +++ b/waspc/packages/wasp-config/.prettierrc.json @@ -0,0 +1,6 @@ +{ + "trailingComma": "es5", + "tabWidth": 2, + "semi": false, + "singleQuote": true +} diff --git a/waspc/packages/wasp-config/eslint.config.js b/waspc/packages/wasp-config/eslint.config.js new file mode 100644 index 0000000000..8a580849a5 --- /dev/null +++ b/waspc/packages/wasp-config/eslint.config.js @@ -0,0 +1,26 @@ +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/"], + }, + { + rules: { + "@typescript-eslint/no-unused-vars": "warn", + "@typescript-eslint/no-empty-function": "warn", + "no-empty": "warn", + "no-constant-condition": "warn", + }, + }, +]; diff --git a/waspc/packages/wasp-config/nodemon.json b/waspc/packages/wasp-config/nodemon.json new file mode 100644 index 0000000000..58bfadc286 --- /dev/null +++ b/waspc/packages/wasp-config/nodemon.json @@ -0,0 +1,6 @@ +{ + "watch": [ + "./src/**/*.ts" + ], + "exec": "tsc || exit 1" +} diff --git a/waspc/packages/wasp-config/package-lock.json b/waspc/packages/wasp-config/package-lock.json new file mode 100644 index 0000000000..b83bb3ef0f --- /dev/null +++ b/waspc/packages/wasp-config/package-lock.json @@ -0,0 +1,1870 @@ +{ + "name": "wasp-config", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "wasp-config", + "version": "1.0.0", + "license": "MIT", + "bin": { + "wasp-config": "dist/run.js" + }, + "devDependencies": { + "@eslint/js": "^9.9.0", + "@types/node": "^22.4.1", + "eslint": "^9.9.0", + "globals": "^15.9.0", + "nodemon": "^3.1.4", + "prettier": "3.3.3", + "typescript": "^5.5.4", + "typescript-eslint": "^8.1.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.0.tgz", + "integrity": "sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.17.1.tgz", + "integrity": "sha512-BlYOpej8AQ8Ev9xVqroV7a02JK3SkBAaN9GfMMH9W6Ch8FlQlkjGw4Ir7+FgYwfirivAf4t+GtzuAxqfukmISA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.4", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz", + "integrity": "sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.9.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.9.0.tgz", + "integrity": "sha512-hhetes6ZHP3BlXLxmd8K2SNgkhNSi+UcecbnwWKwpP7kyi/uC75DJ1lOOBO3xrC4jyojtGE3YxKZPHfk4yrgug==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.4.tgz", + "integrity": "sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.0.tgz", + "integrity": "sha512-d2CGZR2o7fS6sWB7DG/3a95bGKQyHMACZ5aW8qGkkqQpUoZV6C0X7Pc7l4ZNMZkfNBf4VWNe9E1jRsf0G146Ew==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@types/node": { + "version": "22.4.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.4.1.tgz", + "integrity": "sha512-1tbpb9325+gPnKK0dMm+/LMriX0vKxf6RnB0SZUqfyVkQ4fMgUSySqhxE/y8Jvs4NyF1yHzTfG9KlnkIODxPKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.1.0.tgz", + "integrity": "sha512-LlNBaHFCEBPHyD4pZXb35mzjGkuGKXU5eeCA1SxvHfiRES0E82dOounfVpL4DCqYvJEKab0bZIA0gCRpdLKkCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.1.0", + "@typescript-eslint/type-utils": "8.1.0", + "@typescript-eslint/utils": "8.1.0", + "@typescript-eslint/visitor-keys": "8.1.0", + "graphemer": "^1.4.0", + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", + "eslint": "^8.57.0 || ^9.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.1.0.tgz", + "integrity": "sha512-U7iTAtGgJk6DPX9wIWPPOlt1gO57097G06gIcl0N0EEnNw8RGD62c+2/DiP/zL7KrkqnnqF7gtFGR7YgzPllTA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/scope-manager": "8.1.0", + "@typescript-eslint/types": "8.1.0", + "@typescript-eslint/typescript-estree": "8.1.0", + "@typescript-eslint/visitor-keys": "8.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.1.0.tgz", + "integrity": "sha512-DsuOZQji687sQUjm4N6c9xABJa7fjvfIdjqpSIIVOgaENf2jFXiM9hIBZOL3hb6DHK9Nvd2d7zZnoMLf9e0OtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.1.0", + "@typescript-eslint/visitor-keys": "8.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.1.0.tgz", + "integrity": "sha512-oLYvTxljVvsMnldfl6jIKxTaU7ok7km0KDrwOt1RHYu6nxlhN3TIx8k5Q52L6wR33nOwDgM7VwW1fT1qMNfFIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "8.1.0", + "@typescript-eslint/utils": "8.1.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.1.0.tgz", + "integrity": "sha512-q2/Bxa0gMOu/2/AKALI0tCKbG2zppccnRIRCW6BaaTlRVaPKft4oVYPp7WOPpcnsgbr0qROAVCVKCvIQ0tbWog==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.1.0.tgz", + "integrity": "sha512-NTHhmufocEkMiAord/g++gWKb0Fr34e9AExBRdqgWdVBaKoei2dIyYKD9Q0jBnvfbEA5zaf8plUFMUH6kQ0vGg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "8.1.0", + "@typescript-eslint/visitor-keys": "8.1.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.1.0.tgz", + "integrity": "sha512-ypRueFNKTIFwqPeJBfeIpxZ895PQhNyH4YID6js0UoBImWYoSjBsahUn9KMiJXh94uOjVBgHD9AmkyPsPnFwJA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "8.1.0", + "@typescript-eslint/types": "8.1.0", + "@typescript-eslint/typescript-estree": "8.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.1.0.tgz", + "integrity": "sha512-ba0lNI19awqZ5ZNKh6wCModMwoZs457StTebQ0q1NP58zSi2F6MOZRXwfKZy+jB78JNJ/WH8GSh2IQNzXX8Nag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.1.0", + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/acorn": { + "version": "8.12.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", + "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.9.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.9.0.tgz", + "integrity": "sha512-JfiKJrbx0506OEerjK2Y1QlldtBxkAlLxT5OEcRF8uaQ86noDe2k31Vw9rnSWv+MXZHj7OOUV/dA0AhdLFcyvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.11.0", + "@eslint/config-array": "^0.17.1", + "@eslint/eslintrc": "^3.1.0", + "@eslint/js": "9.9.0", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.3.0", + "@nodelib/fs.walk": "^1.2.8", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.0.2", + "eslint-visitor-keys": "^4.0.0", + "espree": "^10.1.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.0.2.tgz", + "integrity": "sha512-6E4xmrTw5wtxnLA5wYL3WDfhZ/1bUBGOXV0zQvVRDOtrR8D0p6W7fs3JweNYhwRYeGvd/1CKX2se0/2s7Q/nJA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz", + "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/espree": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.1.0.tgz", + "integrity": "sha512-M1M6CpiE6ffoigIOWYO9UDP8TMUw9kqb21tf+08IgDYjCsOvCuDt4jQcZmoYxx+w7zlKw9/N0KXfto+I8/FrXA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.12.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/globals": { + "version": "15.9.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.9.0.tgz", + "integrity": "sha512-SmSKyLLKFbSr6rptvP8izbyxJL4ILwqO9Jg23UA0sDlGlu58V59D1//I3vlc0KJphVdUR7vMjHIplYnzBxorQA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true, + "license": "ISC" + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", + "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/nodemon": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.4.tgz", + "integrity": "sha512-wjPBbFhtpJwmIeY2yP7QF+UKzPfltVGtfce1g/bB15/8vCGZj8uxD62b/b9M9/WVgme0NZudpownKN+c0plXlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", + "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true, + "license": "MIT" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true, + "license": "MIT" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true, + "license": "ISC", + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/ts-api-utils": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", + "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "5.5.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", + "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.1.0.tgz", + "integrity": "sha512-prB2U3jXPJLpo1iVLN338Lvolh6OrcCZO+9Yv6AR+tvegPPptYCDBIHiEEUdqRi8gAv2bXNKfMUrgAd2ejn/ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.1.0", + "@typescript-eslint/parser": "8.1.0", + "@typescript-eslint/utils": "8.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "6.19.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.6.tgz", + "integrity": "sha512-e/vggGopEfTKSvj4ihnOLTsqhrKRN3LeO6qSN/GxohhuRv8qH9bNQ4B8W7e/vFL+0XTnmHPB4/kegunZGA4Org==", + "dev": true, + "license": "MIT" + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/waspc/packages/wasp-config/package.json b/waspc/packages/wasp-config/package.json new file mode 100644 index 0000000000..3776e0eeb0 --- /dev/null +++ b/waspc/packages/wasp-config/package.json @@ -0,0 +1,27 @@ +{ + "name": "wasp-config", + "license": "MIT", + "author": "wasp", + "version": "1.0.0", + "description": "Wasp TS SDK", + "type": "module", + "main": "dist/run.js", + "scripts": { + "build": "tsc", + "dev": "nodemon ./src/index.ts" + }, + "bin": "./dist/run.js", + "devDependencies": { + "@eslint/js": "^9.9.0", + "@types/node": "^22.4.1", + "eslint": "^9.9.0", + "globals": "^15.9.0", + "nodemon": "^3.1.4", + "prettier": "3.3.3", + "typescript": "^5.5.4", + "typescript-eslint": "^8.1.0" + }, + "exports": { + ".": "./dist/index.js" + } +} diff --git a/waspc/packages/wasp-config/src/_private.ts b/waspc/packages/wasp-config/src/_private.ts new file mode 100644 index 0000000000..055b619d50 --- /dev/null +++ b/waspc/packages/wasp-config/src/_private.ts @@ -0,0 +1 @@ +export const GET_USER_SPEC = Symbol('GET_USER_SPEC') diff --git a/waspc/packages/wasp-config/src/appSpec.ts b/waspc/packages/wasp-config/src/appSpec.ts new file mode 100644 index 0000000000..074c620c28 --- /dev/null +++ b/waspc/packages/wasp-config/src/appSpec.ts @@ -0,0 +1,210 @@ +/** This module is a mirror implementation of 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 } + +// 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 +} + +export type Route = { + path: string + to: Ref<'Page'> +} + +export type Action = { + fn: ExtImport + entities?: Ref<'Entity'>[] + auth?: boolean +} + +export type Query = { + fn: ExtImport + entities?: Ref<'Entity'>[] + auth?: boolean +} + +export type Job = { + executor: JobExecutor + perform: Perform + schedule?: Schedule + entities?: Ref<'Entity'>[] +} +export type Schedule = { + cron: string + args?: object + executorOptions?: ExecutorOptions +} + +export type Perform = { + fn: ExtImport + executorOptions?: ExecutorOptions +} + +export type Api = { + fn: ExtImport + middlewareConfigFn?: ExtImport + entities?: Ref<'Entity'>[] + httpRoute: HttpRoute + auth?: boolean +} + +export type ApiNamespace = { + middlewareConfigFn: ExtImport + path: string +} + +export type Crud = { + entity: Ref<'Entity'> + operations: CrudOperations +} + +export type App = { + wasp: Wasp + title: string + head?: string[] + auth?: Auth + server?: Server + client?: Client + db?: Db + emailSender?: EmailSender + webSocket?: WebSocket +} + +export type ExtImport = { + kind: 'named' | 'default' + name: string + path: `@src/${string}` +} + +export type JobExecutor = 'PgBoss' + +export type ExecutorOptions = { + pgBoss?: 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 +} + +export type CrudOperationOptions = { + isPublic?: boolean + overrideFn?: ExtImport +} + +export type Wasp = { + // TODO: Check semver in export type system? + version: string +} + +export type Auth = { + userEntity: Ref<'Entity'> + externalAuthEntity?: Ref<'Entity'> + methods: AuthMethods + onAuthFailedRedirectTo: string + onAuthSucceededRedirectTo?: string + onBeforeSignup?: ExtImport + onAfterSignup?: ExtImport + onBeforeOAuthRedirect?: ExtImport + onBeforeLogin?: ExtImport + onAfterLogin?: ExtImport +} + +export type AuthMethods = { + usernameAndPassword?: UsernameAndPasswordConfig + discord?: ExternalAuthConfig + google?: ExternalAuthConfig + gitHub?: ExternalAuthConfig + keycloak?: ExternalAuthConfig + email?: EmailAuthConfig +} + +export type UsernameAndPasswordConfig = { + userSignupFields?: ExtImport +} + +export type ExternalAuthConfig = { + configFn?: ExtImport + userSignupFields?: ExtImport +} + +export type EmailAuthConfig = { + userSignupFields?: ExtImport + fromField: EmailFromField + emailVerification: EmailVerificationConfig + passwordReset: PasswordResetConfig +} + +export type EmailSender = { + provider: EmailProvider + defaultFrom?: EmailFromField +} + +// TODO: duplication +export type EmailProvider = 'SMTP' | 'SendGrid' | 'Mailgun' | 'Dummy' + +export type EmailFromField = { + name?: string + email: string +} + +export type EmailVerificationConfig = { + getEmailContentFn?: ExtImport + clientRoute: Ref<'Route'> +} + +export type PasswordResetConfig = { + getEmailContentFn?: ExtImport + clientRoute: Ref<'Route'> +} + +export type Ref = { + name: string + declType: T +} + +export type Server = { + setupFn?: ExtImport + middlewareConfigFn?: ExtImport +} + +export type Client = { + setupFn?: ExtImport + rootComponent?: ExtImport + baseDir?: `/${string}` +} + +export type Db = { + seeds?: ExtImport[] +} + +export type WebSocket = { + fn: ExtImport + autoConnect?: boolean +} diff --git a/waspc/packages/wasp-config/src/index.ts b/waspc/packages/wasp-config/src/index.ts new file mode 100644 index 0000000000..5df8fea00f --- /dev/null +++ b/waspc/packages/wasp-config/src/index.ts @@ -0,0 +1 @@ +export * from './userApi.js' diff --git a/waspc/packages/wasp-config/src/mappers.ts b/waspc/packages/wasp-config/src/mappers.ts new file mode 100644 index 0000000000..ef46d22bad --- /dev/null +++ b/waspc/packages/wasp-config/src/mappers.ts @@ -0,0 +1,449 @@ +/** This module maps the user-facing API to the internal representation of the app (AppSpec Decl). + */ +import * as AppSpec from './appSpec.js' +import * as User from './userApi.js' + +export function mapUserSpecToDecls( + spec: User.UserSpec, + entityNames: string[] +): AppSpec.Decl[] { + const { + app, + actions, + apis, + apiNamespaces, + auth, + client, + db, + emailSender, + jobs, + pages, + queries, + routes, + server, + websocket, + } = 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', + declName: app.name, + declValue: mapApp( + app.config, + parseEntityRef, + parseRouteRef, + auth, + server, + client, + db, + 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), + }) + } + + for (const [crudName, crudConfig] of spec.cruds.entries()) { + decls.push({ + declType: 'Crud', + declName: crudName, + declValue: mapCrud(crudConfig, parseEntityRef), + }) + } + + return decls +} + +function mapOperationConfig( + config: User.QueryConfig, + parseEntityRef: RefParser<'Entity'> +): AppSpec.Query +function mapOperationConfig( + config: User.ActionConfig, + parseEntityRef: RefParser<'Entity'> +): AppSpec.Action +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, + } +} + +function mapExtImport(extImport: User.ExtImport): AppSpec.ExtImport { + if ('import' in extImport) { + return { kind: 'named', name: extImport.import, path: extImport.from }; + } else if ('importDefault' in extImport) { + return { kind: 'default', name: extImport.importDefault, path: extImport.from }; + } else { + const _exhaustiveCheck: never = extImport; + throw new Error('Invalid ExtImport: neither `import` nor `importDefault` is defined'); + } +} + +function mapApiConfig( + config: User.ApiConfig, + parseEntityRef: RefParser<'Entity'> +): AppSpec.Api { + const { fn, middlewareConfigFn, entities, httpRoute, auth } = config + return { + fn: mapExtImport(fn), + middlewareConfigFn: middlewareConfigFn && mapExtImport(middlewareConfigFn), + ...(entities && { entities: entities.map(parseEntityRef) }), + httpRoute: httpRoute, + auth: auth, + } +} + +function mapApiNamespace( + config: User.ApiNamespaceConfig +): AppSpec.ApiNamespace { + const { middlewareConfigFn, path } = config + return { + middlewareConfigFn: mapExtImport(middlewareConfigFn), + path, + } +} + +function mapApp( + app: User.AppConfig, + // TODO: Make this better, optional props are problematic so I have to pass the parsers first + parseEntityRef: RefParser<'Entity'>, + parseRouteRef: RefParser<'Route'>, + auth?: User.AuthConfig, + server?: User.ServerConfig, + client?: User.ClientConfig, + db?: User.DbConfig, + emailSender?: User.EmailSenderConfig, + webSocket?: User.WebsocketConfig +): AppSpec.App { + const { title, wasp, head } = app + return { + wasp, + title, + head, + auth: auth && mapAuth(auth, parseEntityRef, parseRouteRef), + server: server && mapServer(server), + client: client && mapClient(client), + webSocket: webSocket && mapWebSocket(webSocket), + db: db && mapDb(db), + emailSender: emailSender && mapEmailSender(emailSender), + } +} + +function mapAuth( + auth: User.AuthConfig, + parseEntityRef: RefParser<'Entity'>, + parseRouteRef: RefParser<'Route'> +): AppSpec.Auth { + const { + userEntity, + externalAuthEntity, + methods, + onAuthFailedRedirectTo, + onAuthSucceededRedirectTo, + onBeforeSignup, + onAfterSignup, + onBeforeOAuthRedirect, + onBeforeLogin, + onAfterLogin, + } = auth + return { + userEntity: parseEntityRef(userEntity), + // TODO: Abstract away this pattern + ...(externalAuthEntity && { + externalAuthEntity: parseEntityRef(externalAuthEntity), + }), + methods: mapAuthMethods(methods, parseRouteRef), + onAuthFailedRedirectTo, + onAuthSucceededRedirectTo, + onBeforeSignup: onBeforeSignup && mapExtImport(onBeforeSignup), + onAfterSignup: onAfterSignup && mapExtImport(onAfterSignup), + onBeforeOAuthRedirect: + onBeforeOAuthRedirect && mapExtImport(onBeforeOAuthRedirect), + onBeforeLogin: onBeforeLogin && mapExtImport(onBeforeLogin), + onAfterLogin: onAfterLogin && mapExtImport(onAfterLogin), + } +} + +function mapAuthMethods( + methods: User.AuthMethods, + parseRouteRef: RefParser<'Route'> +): AppSpec.AuthMethods { + // TODO: check keyof danger, effective ts + const { usernameAndPassword, discord, google, gitHub, keycloak, email } = + methods + return { + usernameAndPassword: + usernameAndPassword && mapUsernameAndPassword(usernameAndPassword), + discord: discord && mapExternalAuth(discord), + google: google && mapExternalAuth(google), + gitHub: gitHub && mapExternalAuth(gitHub), + keycloak: keycloak && mapExternalAuth(keycloak), + email: email && mapEmailAuth(email, parseRouteRef), + } +} + +function mapUsernameAndPassword( + usernameAndPassword: User.UsernameAndPasswordConfig +): AppSpec.UsernameAndPasswordConfig { + const { userSignupFields } = usernameAndPassword + return { + userSignupFields: userSignupFields && mapExtImport(userSignupFields), + } +} + +export function mapExternalAuth( + externalAuth: User.ExternalAuthConfig +): AppSpec.ExternalAuthConfig { + const { configFn, userSignupFields } = externalAuth + return { + configFn: configFn && mapExtImport(configFn), + userSignupFields: userSignupFields && mapExtImport(userSignupFields), + } +} + +function mapEmailAuth( + email: User.EmailAuthConfig, + parseRouteRef: RefParser<'Route'> +): AppSpec.EmailAuthConfig { + const { userSignupFields, fromField, emailVerification, passwordReset } = + email + return { + userSignupFields: userSignupFields && mapExtImport(userSignupFields), + fromField, + emailVerification: mapEmailVerification(emailVerification, parseRouteRef), + passwordReset: mapPasswordReset(passwordReset, parseRouteRef), + } +} + +function mapEmailVerification( + emailVerification: User.EmailVerificationConfig, + parseRouteRef: RefParser<'Route'> +): AppSpec.EmailVerificationConfig { + const { getEmailContentFn, clientRoute } = emailVerification + return { + getEmailContentFn: getEmailContentFn && mapExtImport(getEmailContentFn), + clientRoute: parseRouteRef(clientRoute), + } +} + +export function mapPasswordReset( + passwordReset: User.PasswordResetConfig, + parseRouteRef: RefParser<'Route'> +): AppSpec.PasswordResetConfig { + const { getEmailContentFn, clientRoute } = passwordReset + return { + getEmailContentFn: getEmailContentFn && mapExtImport(getEmailContentFn), + clientRoute: parseRouteRef(clientRoute), + } +} + +function mapDb(db: User.DbConfig): AppSpec.Db { + const { seeds } = db + return { + seeds: seeds && seeds.map(mapExtImport), + } +} + +function mapEmailSender( + emailSender: User.EmailSenderConfig +): AppSpec.EmailSender { + return emailSender +} + +function mapServer(server: User.ServerConfig): AppSpec.Server { + const { setupFn, middlewareConfigFn } = server + return { + setupFn: mapExtImport(setupFn), + middlewareConfigFn: mapExtImport(middlewareConfigFn), + } +} + +function mapClient(client: User.ClientConfig): AppSpec.Client { + const { setupFn, rootComponent } = client + return { + setupFn: mapExtImport(setupFn), + rootComponent: mapExtImport(rootComponent), + } +} + +function mapWebSocket(websocket: User.WebsocketConfig): AppSpec.WebSocket { + const { fn, autoConnect } = websocket + return { + fn: mapExtImport(fn), + autoConnect, + } +} + +function mapJob( + job: User.JobConfig, + parseEntityRef: RefParser<'Entity'> +): AppSpec.Job { + const { executor, perform, schedule, entities } = job + return { + executor: executor, + perform: mapPerform(perform), + schedule: schedule && mapSchedule(schedule), + ...(entities && { entities: entities.map(parseEntityRef) }), + } +} + +function mapSchedule(schedule: User.ScheduleConfig): AppSpec.Schedule { + const { cron, args, executorOptions } = schedule + return { + cron, + args, + executorOptions, + } +} +function mapPerform(perform: User.Perform): AppSpec.Perform { + const { fn, executorOptions } = perform + return { + fn: mapExtImport(fn), + ...(executorOptions && { executorOptions }), + } +} + +function mapRoute( + route: User.RouteConfig, + parsePageRef: RefParser<'Page'> +): AppSpec.Route { + const { path, to } = route + return { + path, + to: parsePageRef(to), + } +} +function mapCrud( + crudConfig: User.Crud, + parseEntityRef: RefParser<'Entity'> +): AppSpec.Crud { + const { entity, operations } = crudConfig + return { + entity: parseEntityRef(entity), + operations: mapCrudOperations(operations), + } +} + +function mapCrudOperations( + operations: User.CrudOperations +): AppSpec.CrudOperations { + const { get, getAll, create, update, delete: del } = operations + // TODO: Do this for all keys + return { + get: get && mapCrudOperationOptions(get), + getAll: getAll && mapCrudOperationOptions(getAll), + create: create && mapCrudOperationOptions(create), + update: update && mapCrudOperationOptions(update), + delete: del && mapCrudOperationOptions(del), + } +} + +function mapCrudOperationOptions( + options: User.CrudOperationOptions +): AppSpec.CrudOperationOptions { + const { isPublic, overrideFn } = options + return { + isPublic, + overrideFn: overrideFn && mapExtImport(overrideFn), + } +} + +function mapPage(pageConfig: User.PageConfig): AppSpec.Page { + const { component, authRequired } = pageConfig + return { + component: mapExtImport(component), + authRequired, + } +} + +type RefParser = ( + potentialReferences: string +) => AppSpec.Ref + +function makeRefParser( + declType: T, + declNames: string[] +): RefParser { + return function parseRef(potentialRef: string): AppSpec.Ref { + if (!declNames.includes(potentialRef)) { + throw new Error(`Invalid ${declType} reference: ${potentialRef}`) + } + return { + name: potentialRef, + declType, + } as AppSpec.Ref + } +} diff --git a/waspc/packages/wasp-config/src/run.ts b/waspc/packages/wasp-config/src/run.ts new file mode 100644 index 0000000000..b810dc08bf --- /dev/null +++ b/waspc/packages/wasp-config/src/run.ts @@ -0,0 +1,90 @@ +#!/usr/bin/env node +import { writeFileSync } from 'fs' +import { App } from './userApi.js' +import { Decl } from './appSpec.js' +import { mapUserSpecToDecls } from './mappers.js' +import { GET_USER_SPEC } from './_private.js' +import { exit } from 'process' + +main() + +async function main() { + const { mainWaspJs, outputFile, entityNames } = parseProcessArguments( + process.argv + ) + + const app = await importApp(mainWaspJs) + const spec = analyzeApp(app, entityNames) + + writeFileSync(outputFile, serialize(spec)) +} + +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 +} + +function isApp(app: unknown): app is App { + return app instanceof App +} + +function analyzeApp(app: App, entityNames: string[]): Decl[] { + const userSpec = app[GET_USER_SPEC]() + return mapUserSpecToDecls(userSpec, entityNames) +} + +function parseProcessArguments(args: string[]): { + mainWaspJs: string + outputFile: string + entityNames: string[] +} { + if (args.length < 5) { + throw new Error( + 'Usage: node run.js ' + ) + } + + const [_node, _runjs, mainWaspJs, outputFile, entityNamesJson] = process.argv + if ( + typeof mainWaspJs !== 'string' || + typeof outputFile !== 'string' || + typeof entityNamesJson !== 'string' + ) { + throw new Error( + 'Usage: node run.js ' + ) + } + + const entityNames = parseEntityNamesJson(entityNamesJson) + + return { + mainWaspJs, + outputFile, + entityNames, + } +} + +function parseEntityNamesJson(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.') + } + return entities +} + +function serialize(appConfig: Decl[]): string { + return JSON.stringify(appConfig) +} diff --git a/waspc/packages/wasp-config/src/userApi.ts b/waspc/packages/wasp-config/src/userApi.ts new file mode 100644 index 0000000000..527a1ec61c --- /dev/null +++ b/waspc/packages/wasp-config/src/userApi.ts @@ -0,0 +1,281 @@ +/** This module defines the user-facing API for defining a Wasp app. + */ +import * as AppSpec from './appSpec.js' +import { GET_USER_SPEC } from './_private.js' + +export class App { + #userSpec: UserSpec; + + // NOTE: Using a non-public symbol gives us a pacakge-private property. + [GET_USER_SPEC]() { + return this.#userSpec + } + + constructor(name: string, config: AppConfig) { + this.#userSpec = { + app: { name, config: config }, + actions: new Map(), + apiNamespaces: new Map(), + apis: new Map(), + auth: undefined, + client: undefined, + cruds: new Map(), + db: undefined, + emailSender: undefined, + jobs: new Map(), + pages: new Map(), + queries: new Map(), + routes: new Map(), + server: undefined, + websocket: undefined, + } + } + + // TODO: Enforce that all methods are covered in compile time + action(this: App, name: string, config: ActionConfig): void { + this.#userSpec.actions.set(name, config) + } + + apiNamespace(this: App, name: string, config: ApiNamespaceConfig): void { + this.#userSpec.apiNamespaces.set(name, config) + } + + api(this: App, name: string, config: ApiConfig): void { + this.#userSpec.apis.set(name, config) + } + + auth(this: App, config: AuthConfig): void { + this.#userSpec.auth = config + } + + client(this: App, config: ClientConfig): void { + this.#userSpec.client = config + } + + crud(this: App, name: string, config: Crud): void { + this.#userSpec.cruds.set(name, config) + } + + db(this: App, config: DbConfig): void { + this.#userSpec.db = config + } + + emailSender(this: App, config: EmailSenderConfig): void { + this.#userSpec.emailSender = config + } + + job(this: App, name: string, config: JobConfig): void { + this.#userSpec.jobs.set(name, config) + } + + page(this: App, name: string, config: PageConfig): PageName { + this.#userSpec.pages.set(name, config) + return name as PageName + } + + query(this: App, name: string, config: QueryConfig): void { + this.#userSpec.queries.set(name, config) + } + + route(this: App, name: string, config: RouteConfig): void { + this.#userSpec.routes.set(name, config) + } + + server(this: App, config: ServerConfig): void { + this.#userSpec.server = config + } + + webSocket(this: App, config: WebsocketConfig) { + this.#userSpec.websocket = config + } +} + +export type WaspConfig = AppSpec.Wasp + +export type AppConfig = Pick + +export type ExtImport = { + import: string + from: AppSpec.ExtImport['path'] +} | { + importDefault: string + from: AppSpec.ExtImport['path'] +} + +export type ServerConfig = { + setupFn: ExtImport + middlewareConfigFn: ExtImport +} + +export type PageConfig = { + component: ExtImport + authRequired?: boolean +} + +export type WebsocketConfig = { + fn: ExtImport + autoConnect?: boolean +} + +export type ClientConfig = { + rootComponent: ExtImport + setupFn: ExtImport +} + +export type DbConfig = { + seeds?: ExtImport[] +} + +export type RouteConfig = { + path: string + to: PageName +} + +type PageName = string & { _brand: 'Page' } + +export type ActionConfig = { + fn: ExtImport + entities?: string[] + auth?: boolean +} + +export type ApiNamespaceConfig = { + middlewareConfigFn: ExtImport + path: string +} + +export type ApiConfig = { + fn: ExtImport + middlewareConfigFn?: ExtImport + entities?: string[] + httpRoute: AppSpec.HttpRoute + auth?: boolean +} + +export type JobConfig = { + executor: AppSpec.JobExecutor + perform: Perform + schedule?: ScheduleConfig + entities?: string[] +} + +export type Crud = { + entity: string + operations: CrudOperations +} + +export type CrudOperations = { + get?: CrudOperationOptions + getAll?: CrudOperationOptions + create?: CrudOperationOptions + update?: CrudOperationOptions + delete?: CrudOperationOptions +} + +export type CrudOperationOptions = { + isPublic?: boolean + overrideFn?: ExtImport +} + +export type Perform = { + fn: ExtImport + executorOptions?: ExecutorOptions +} + +export type ScheduleConfig = { + cron: string + args?: object + executorOptions?: ExecutorOptions +} + +type ExecutorOptions = { + // TODO: Type this better and test it + // rewriting waspc/todoApp should make sure it works + pgBoss: object +} + +export type QueryConfig = { + fn: ExtImport + entities?: string[] + auth?: boolean +} + +export type EmailSenderConfig = AppSpec.EmailSender + +export type AuthConfig = { + userEntity: string + methods: AuthMethods + externalAuthEntity?: string + onAuthFailedRedirectTo: string + onAuthSucceededRedirectTo?: string + onBeforeSignup?: ExtImport + onAfterSignup?: ExtImport + onBeforeOAuthRedirect?: ExtImport + onBeforeLogin?: ExtImport + onAfterLogin?: ExtImport +} + +export type AuthMethods = { + usernameAndPassword?: UsernameAndPasswordConfig + discord?: ExternalAuthConfig + google?: ExternalAuthConfig + gitHub?: ExternalAuthConfig + keycloak?: ExternalAuthConfig + email?: EmailAuthConfig +} + +export type UsernameAndPasswordConfig = { + userSignupFields?: ExtImport +} + +export type ExternalAuthConfig = { + configFn?: ExtImport + userSignupFields?: ExtImport +} + +export type EmailAuthConfig = { + userSignupFields?: ExtImport + fromField: EmailFromField + emailVerification: EmailVerificationConfig + passwordReset: PasswordResetConfig +} + +export type EmailSender = { + provider: EmailProvider + defaultFrom?: EmailFromField +} + +export type EmailProvider = AppSpec.EmailProvider + +export type EmailFromField = { + name?: string + email: string +} + +export type EmailVerificationConfig = { + getEmailContentFn?: ExtImport + clientRoute: string +} + +export type PasswordResetConfig = { + getEmailContentFn?: ExtImport + clientRoute: string +} + +export type UserSpec = { + app: { name: string; config: AppConfig } + actions: Map + apiNamespaces: Map + apis: Map + auth?: AuthConfig + client?: ClientConfig + cruds: Map + db?: DbConfig + emailSender?: EmailSenderConfig + jobs: Map + pages: Map + queries: Map + routes: Map + server?: ServerConfig + websocket?: WebsocketConfig +} diff --git a/waspc/packages/wasp-config/tsconfig.json b/waspc/packages/wasp-config/tsconfig.json new file mode 100644 index 0000000000..0224b8b38b --- /dev/null +++ b/waspc/packages/wasp-config/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "esModuleInterop": true, + "skipLibCheck": true, + "target": "es2022", + "allowJs": true, + "resolveJsonModule": true, + "moduleDetection": "force", + "isolatedModules": true, + + // linting + "strict": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + + "module": "NodeNext", + "outDir": "dist", + "declarationMap": true, + + "declaration": true, + "lib": ["es2022"] + }, + // better structure in output + "include": ["src"], +} diff --git a/waspc/src/Wasp/Analyzer.hs b/waspc/src/Wasp/Analyzer.hs index 3f86d3407c..8c4c98a68a 100644 --- a/waspc/src/Wasp/Analyzer.hs +++ b/waspc/src/Wasp/Analyzer.hs @@ -111,6 +111,7 @@ module Wasp.Analyzer -- * API analyze, + getEntityDecls, takeDecls, AnalyzeError (..), getErrorMessageAndCtx, @@ -127,8 +128,9 @@ import Wasp.Analyzer.AnalyzeError ) import Wasp.Analyzer.Evaluator (Decl, evaluate, takeDecls) import Wasp.Analyzer.Parser (parseStatements) +import qualified Wasp.Analyzer.Parser as Parser import Wasp.Analyzer.Parser.Valid (validateAst) -import Wasp.Analyzer.Prisma (injectEntitiesFromPrismaSchema) +import Wasp.Analyzer.Prisma (injectEntitiesFromPrismaSchema, parseEntityStatements) import Wasp.Analyzer.StdTypeDefinitions (stdTypes) import Wasp.Analyzer.TypeChecker (typeCheck) import qualified Wasp.Psl.Ast.Schema as Psl.Schema @@ -157,7 +159,17 @@ analyze prismaSchemaAst = ^ disallow entities here ^ inject entities here --} - >=> (left ((: []) . ValidationError) . validateAst) + >=> wrapAnalyzerError ValidationError . validateAst >=> injectEntitiesFromPrismaSchema prismaSchemaAst - >=> (left ((: []) . TypeError) . typeCheck stdTypes) - >=> (left ((: []) . EvaluationError) . evaluate stdTypes) + >=> (wrapAnalyzerError TypeError . typeCheck stdTypes) + >=> (wrapAnalyzerError EvaluationError . evaluate stdTypes) + +getEntityDecls :: Psl.Schema.Schema -> Either [AnalyzeError] [Decl] +getEntityDecls schema = + wrapAnalyzerError TypeError (typeCheck stdTypes astWithEntitiesOnly) + >>= (wrapAnalyzerError EvaluationError . evaluate stdTypes) + where + astWithEntitiesOnly = Parser.AST $ parseEntityStatements schema + +wrapAnalyzerError :: (e -> AnalyzeError) -> Either e a -> Either [AnalyzeError] a +wrapAnalyzerError makeError = left ((: []) . makeError) diff --git a/waspc/src/Wasp/Analyzer/Evaluator/Evaluation/TypedExpr/Combinators.hs b/waspc/src/Wasp/Analyzer/Evaluator/Evaluation/TypedExpr/Combinators.hs index 26598d58e2..8e6c70c7bd 100644 --- a/waspc/src/Wasp/Analyzer/Evaluator/Evaluation/TypedExpr/Combinators.hs +++ b/waspc/src/Wasp/Analyzer/Evaluator/Evaluation/TypedExpr/Combinators.hs @@ -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. -- 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. -- So we might want to move it to Parser at some point in the future, if we -- figure out that is better (it sounds/feels like it could be). - case stripImportPrefix extImportPath of - Just relFileFP -> case SP.parseRelFileP relFileFP of - Left err -> mkParseError ctx $ show err - Right relFileSP -> pure $ AppSpec.ExtImport.ExtImport name relFileSP - Nothing -> - mkParseError - ctx - $ "Path in external import must start with \"" ++ extSrcPrefix ++ "\"!" + case AppSpec.ExtImport.parseExtImportPath extImportPath of + Left err -> mkParseError ctx err + Right importPath -> pure $ AppSpec.ExtImport.ExtImport name importPath expr -> Left $ ER.mkEvaluationError ctx $ ER.ExpectedType T.ExtImportType (TypedAST.exprType expr) where mkParseError ctx msg = Left $ ER.mkEvaluationError ctx $ ER.ParseError $ ER.EvaluationParseError msg - stripImportPrefix importPath = stripPrefix extSrcPrefix importPath - -- Filip: We no longer want separation between client and server code - -- todo (filip): Do we still want to know whic is which. We might (because of the reloading). - -- For now, as we'd like (expect): - -- - Nodemon watches all files in the user's source folder (client files - -- included), but tsc only compiles the server files (I think because it - -- knows that the others aren't used). I am not yet sure how it knows this. - -- - Vite also only triggers on client files. I am not sure how it knows - -- about the difference either. - -- todo (filip): investigate - extSrcPrefix = "@src/" -- | An evaluation that expects a "JSON". json :: TypedExprEvaluation AppSpec.JSON.JSON diff --git a/waspc/src/Wasp/Analyzer/Prisma.hs b/waspc/src/Wasp/Analyzer/Prisma.hs index ba070ebf49..161222fbec 100644 --- a/waspc/src/Wasp/Analyzer/Prisma.hs +++ b/waspc/src/Wasp/Analyzer/Prisma.hs @@ -1,5 +1,6 @@ module Wasp.Analyzer.Prisma ( injectEntitiesFromPrismaSchema, + parseEntityStatements, ) where @@ -11,9 +12,12 @@ import qualified Wasp.Psl.Generator.Model as Psl.Model.Generator injectEntitiesFromPrismaSchema :: Psl.Schema.Schema -> Parser.AST -> Either a Parser.AST injectEntitiesFromPrismaSchema schema ast = Right $ ast {Parser.astStmts = stmts ++ entityStmts} where - entityStmts = makeEntityStmt <$> generatePrismaModelSources schema + entityStmts = parseEntityStatements schema stmts = Parser.astStmts ast +parseEntityStatements :: Psl.Schema.Schema -> [WithCtx Parser.Stmt] +parseEntityStatements schema = makeEntityStmt <$> generatePrismaModelSources schema + type ModelName = String type ModelBody = String diff --git a/waspc/src/Wasp/AppSpec/App/Wasp.hs b/waspc/src/Wasp/AppSpec/App/Wasp.hs index ab51f28ba1..776ed060e1 100644 --- a/waspc/src/Wasp/AppSpec/App/Wasp.hs +++ b/waspc/src/Wasp/AppSpec/App/Wasp.hs @@ -7,11 +7,8 @@ module Wasp.AppSpec.App.Wasp (Wasp (..)) where import Data.Aeson (FromJSON) import Data.Data (Data) import GHC.Generics (Generic) -import Wasp.AppSpec.Core.IsDecl (IsDecl) data Wasp = Wasp { version :: String } deriving (Show, Eq, Data, Generic, FromJSON) - -instance IsDecl Wasp diff --git a/waspc/src/Wasp/AppSpec/ExtImport.hs b/waspc/src/Wasp/AppSpec/ExtImport.hs index 5de24f10da..fee7606a78 100644 --- a/waspc/src/Wasp/AppSpec/ExtImport.hs +++ b/waspc/src/Wasp/AppSpec/ExtImport.hs @@ -6,14 +6,18 @@ module Wasp.AppSpec.ExtImport ( ExtImport (..), ExtImportName (..), importIdentifier, + parseExtImportPath, ) where +import Control.Arrow (left) import Data.Aeson (FromJSON (parseJSON), withObject, (.:)) import Data.Aeson.Types (ToJSON) import Data.Data (Data) +import Data.List (stripPrefix) import GHC.Generics (Generic) -import StrongPath (File', Path, Posix, Rel, parseRelFileP) +import StrongPath (File', Path, Posix, Rel) +import qualified StrongPath as SP import Wasp.AppSpec.ExternalFiles (SourceExternalCodeDir) data ExtImport = ExtImport @@ -30,7 +34,9 @@ instance FromJSON ExtImport where nameStr <- o .: "name" pathStr <- o .: "path" extImportName <- parseExtImportName kindStr nameStr - extImportPath <- parseExtImportPath pathStr + extImportPath <- case parseExtImportPath pathStr of + Right path' -> pure path' + Left err -> fail err return $ ExtImport extImportName extImportPath where parseExtImportName kindStr nameStr = case kindStr of @@ -38,10 +44,6 @@ instance FromJSON ExtImport where "named" -> pure $ ExtImportField nameStr _ -> fail $ "Failed to parse import kind: " <> kindStr - parseExtImportPath pathStr = case parseRelFileP pathStr of - Just path' -> pure path' - Nothing -> fail $ "Failed to parse relative posix path to file: " <> pathStr - type ExtImportPath = Path Posix (Rel SourceExternalCodeDir) File' type Identifier = String @@ -57,3 +59,23 @@ importIdentifier :: ExtImport -> Identifier importIdentifier (ExtImport importName _) = case importName of ExtImportModule n -> n ExtImportField n -> n + +parseExtImportPath :: String -> Either String ExtImportPath +parseExtImportPath extImportPath = case stripImportPrefix extImportPath of + Nothing -> Left $ "Path in external import must start with \"" ++ extSrcPrefix ++ "\"!" + Just relFileFP -> + left + (("Failed to parse relative posix path to file: " ++) . show) + $ SP.parseRelFileP relFileFP + where + stripImportPrefix importPath = stripPrefix extSrcPrefix importPath + -- Filip: We no longer want separation between client and server code + -- todo (filip): Do we still want to know which is which. We might (because of the reloading). + -- For now, as we'd like (expect): + -- - Nodemon watches all files in the user's source folder (client files + -- included), but tsc only compiles the server files (I think because it + -- knows that the others aren't used). I am not yet sure how it knows this. + -- - Vite also only triggers on client files. I am not sure how it knows + -- about the difference either. + -- todo (filip): investigate + extSrcPrefix = "@src/" diff --git a/waspc/src/Wasp/Error.hs b/waspc/src/Wasp/Error.hs index 947d82fb33..aa4ef8474a 100644 --- a/waspc/src/Wasp/Error.hs +++ b/waspc/src/Wasp/Error.hs @@ -1,8 +1,9 @@ module Wasp.Error (showCompilerErrorForTerminal) where import Data.List (intercalate) -import StrongPath (Abs, File', Path') +import StrongPath (Abs, Path') import qualified StrongPath as SP +import StrongPath.Types (File) import Wasp.Analyzer.Parser.Ctx (Ctx, getCtxRgn) import Wasp.Analyzer.Parser.SourcePosition (SourcePosition (..)) import Wasp.Analyzer.Parser.SourceRegion (SourceRegion (..)) @@ -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 showCompilerErrorForTerminal (waspFilePath, waspFileContent) (errMsg, errCtx) = let srcRegion = getCtxRgn errCtx in intercalate diff --git a/waspc/src/Wasp/Generator/ExternalConfig/TsConfig.hs b/waspc/src/Wasp/Generator/ExternalConfig/TsConfig.hs index ab3d3e4001..8d3612f807 100644 --- a/waspc/src/Wasp/Generator/ExternalConfig/TsConfig.hs +++ b/waspc/src/Wasp/Generator/ExternalConfig/TsConfig.hs @@ -1,7 +1,7 @@ {-# LANGUAGE FlexibleInstances #-} module Wasp.Generator.ExternalConfig.TsConfig - ( validateTsConfig, + ( validateSrcTsConfig, ) where @@ -23,8 +23,8 @@ instance IsJavascriptValue Bool where type FieldName = String -validateTsConfig :: T.TsConfig -> [ErrorMsg] -validateTsConfig tsConfig = +validateSrcTsConfig :: T.TsConfig -> [ErrorMsg] +validateSrcTsConfig tsConfig = concat [ validateRequiredFieldInCompilerOptions "module" "esnext" T._module, validateRequiredFieldInCompilerOptions "target" "esnext" T.target, diff --git a/waspc/src/Wasp/Generator/NpmInstall.hs b/waspc/src/Wasp/Generator/NpmInstall.hs index 316b9fb6e7..560525dce1 100644 --- a/waspc/src/Wasp/Generator/NpmInstall.hs +++ b/waspc/src/Wasp/Generator/NpmInstall.hs @@ -1,5 +1,6 @@ module Wasp.Generator.NpmInstall - ( installNpmDependenciesWithInstallRecord, + ( installProjectNpmDependencies, + installNpmDependenciesWithInstallRecord, ) where diff --git a/waspc/src/Wasp/NodePackageFFI.hs b/waspc/src/Wasp/NodePackageFFI.hs index e31e28eb91..f2ef7ff312 100644 --- a/waspc/src/Wasp/NodePackageFFI.hs +++ b/waspc/src/Wasp/NodePackageFFI.hs @@ -6,8 +6,10 @@ module Wasp.NodePackageFFI -- Provides utilities for setting up and running node processes from the -- @packages/@ directory. - Package (..), + RunnablePackage (..), + InstallablePackage (..), getPackageProcessOptions, + getPackageInstallationPath, ) where @@ -21,7 +23,7 @@ import Wasp.Data (DataDir) import qualified Wasp.Data as Data import qualified Wasp.Node.Version as NodeVersion -data Package +data RunnablePackage = DeployPackage | TsInspectPackage | -- | TODO(martin): I implemented this ts package because I planned to use prisma's TS sdk @@ -34,6 +36,8 @@ data Package PrismaPackage | WaspStudioPackage +data InstallablePackage = WaspConfigPackage + data PackagesDir data PackageDir @@ -43,11 +47,16 @@ data PackageScript packagesDirInDataDir :: Path' (Rel DataDir) (Dir PackagesDir) packagesDirInDataDir = [reldir|packages|] -packageDirInPackagesDir :: Package -> Path' (Rel PackagesDir) (Dir PackageDir) -packageDirInPackagesDir DeployPackage = [reldir|deploy|] -packageDirInPackagesDir TsInspectPackage = [reldir|ts-inspect|] -packageDirInPackagesDir PrismaPackage = [reldir|prisma|] -packageDirInPackagesDir WaspStudioPackage = [reldir|studio|] +runnablePackageDirInPackagesDir :: RunnablePackage -> Path' (Rel PackagesDir) (Dir PackageDir) +runnablePackageDirInPackagesDir = \case + DeployPackage -> [reldir|deploy|] + TsInspectPackage -> [reldir|ts-inspect|] + PrismaPackage -> [reldir|prisma|] + WaspStudioPackage -> [reldir|studio|] + +installablePackageDirInPackagesDir :: InstallablePackage -> Path' (Rel PackagesDir) (Dir PackageDir) +installablePackageDirInPackagesDir = \case + WaspConfigPackage -> [reldir|wasp-config|] scriptInPackageDir :: Path' (Rel PackageDir) (File PackageScript) scriptInPackageDir = [relfile|dist/index.js|] @@ -60,22 +69,30 @@ 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. -getPackageProcessOptions :: Package -> [String] -> IO P.CreateProcess +-- 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 NodeVersion.VersionCheckSuccess -> pure () NodeVersion.VersionCheckFail errorMsg -> do hPutStrLn stderr errorMsg exitFailure - packageDir <- getPackageDir package + packageDir <- getRunnablePackageDir package let scriptFile = packageDir scriptInPackageDir ensurePackageDependenciesAreInstalled packageDir return $ packageCreateProcess packageDir "node" (fromAbsFile scriptFile : args) -getPackageDir :: Package -> IO (Path' Abs (Dir PackageDir)) -getPackageDir package = do +getPackageInstallationPath :: InstallablePackage -> IO String +getPackageInstallationPath package = do + waspDataDir <- Data.getAbsDataDirPath + let absPackagePath = waspDataDir packagesDirInDataDir installablePackageDirInPackagesDir package + return $ fromAbsDir absPackagePath + +getRunnablePackageDir :: RunnablePackage -> IO (Path' Abs (Dir PackageDir)) +getRunnablePackageDir package = do waspDataDir <- Data.getAbsDataDirPath - let packageDir = waspDataDir packagesDirInDataDir packageDirInPackagesDir package + let packageDir = waspDataDir packagesDirInDataDir runnablePackageDirInPackagesDir package return packageDir -- | Runs @npm install@ if @node_modules@ does not exist in the package directory. diff --git a/waspc/src/Wasp/Project/Analyze.hs b/waspc/src/Wasp/Project/Analyze.hs index 5cd19cd96a..0004fad4c9 100644 --- a/waspc/src/Wasp/Project/Analyze.hs +++ b/waspc/src/Wasp/Project/Analyze.hs @@ -3,27 +3,56 @@ module Wasp.Project.Analyze 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', Path', toFilePath, ()) +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, ) import Wasp.Project.Db (makeDevDatabaseUrl) @@ -37,8 +66,9 @@ 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 (maybeToEither) +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 @@ -47,7 +77,7 @@ analyzeWaspProject :: CompileOptions -> IO (Either [CompileError] AS.AppSpec, [CompileWarning]) analyzeWaspProject waspDir options = do - waspFilePathOrError <- maybeToEither [fileNotFoundMessage] <$> findWaspFile waspDir + waspFilePathOrError <- left (: []) <$> findWaspFile waspDir case waspFilePathOrError of Left err -> return (Left err, []) @@ -56,17 +86,116 @@ analyzeWaspProject waspDir options = do (Left prismaSchemaErrors, prismaSchemaWarnings) -> return (Left prismaSchemaErrors, prismaSchemaWarnings) -- NOTE: we are ignoring prismaSchemaWarnings if the schema was parsed successfully (Right prismaSchemaAst, _) -> - analyzeWaspFile prismaSchemaAst waspFilePath >>= \case + analyzeWaspFile waspDir prismaSchemaAst waspFilePath >>= \case Left errors -> return (Left errors, []) Right declarations -> - EC.analyzeExternalConfigs waspDir >>= \case + EC.analyzeExternalConfigs waspDir (getSrcTsConfigInWaspProjectDir waspFilePath) >>= \case 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 - fileNotFoundMessage = "Couldn't find the *.wasp file in the " ++ toFilePath waspDir ++ " directory" + 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 -analyzeWaspFile :: Psl.Schema.Schema -> Path' Abs File' -> IO (Either [CompileError] [AS.Decl]) -analyzeWaspFile prismaSchemaAst waspFilePath = do +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 @@ -116,15 +245,22 @@ constructAppSpec waspDir options externalConfigs parsedPrismaSchema decls = do return $ runValidation ASV.validateAppSpec appSpec -findWaspFile :: Path' Abs (Dir WaspProjectDir) -> IO (Maybe (Path' Abs File')) +findWaspFile :: Path' Abs (Dir WaspProjectDir) -> IO (Either String WaspFilePath) findWaspFile waspDir = do files <- fst <$> IOUtil.listDirectory waspDir - return $ (waspDir ) <$> find isWaspFile files + 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 - isWaspFile path = - ".wasp" - `isSuffixOf` toFilePath path - && (length (toFilePath path) > length (".wasp" :: String)) + 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 @@ -143,7 +279,7 @@ analyzePrismaSchema waspProjectDir = do -- NOTE: linking here to migration docs because I think it's the most common reason why schema.prisma file is missing. -- After people mostly start using 0.14.0+ they will have schema.prisma file, so this message will be less relevant. -- If we see that this message is still relevant, we can change it to be more general. - couldntFindPrismaSchemaMessage = "Couldn't find the schema.prisma file in the " ++ toFilePath waspProjectDir ++ " directory. \nRead more: https://wasp-lang.dev/docs/migrate-from-0-13-to-0-14#migrate-to-the-new-schemaprisma-file" + couldntFindPrismaSchemaMessage = "Couldn't find the schema.prisma file in the " ++ fromAbsDir waspProjectDir ++ " directory. \nRead more: https://wasp-lang.dev/docs/migrate-from-0-13-to-0-14#migrate-to-the-new-schemaprisma-file" runValidation :: (result -> [ValidationError]) -> result -> (Either [CompileError] result, [CompileWarning]) runValidation getErrorsAndWarnings result = diff --git a/waspc/src/Wasp/Project/Common.hs b/waspc/src/Wasp/Project/Common.hs index f6541cd725..9fb166e924 100644 --- a/waspc/src/Wasp/Project/Common.hs +++ b/waspc/src/Wasp/Project/Common.hs @@ -5,7 +5,10 @@ module Wasp.Project.Common CompileError, CompileWarning, PackageJsonFile, - TsConfigFile, + SrcTsConfigFile, + WaspFilePath (..), + WaspLangFile, + WaspTsFile, findFileInWaspProjectDir, dotWaspDirInWaspProjectDir, generatedCodeDirInDotWaspDir, @@ -18,9 +21,10 @@ module Wasp.Project.Common nodeModulesDirInWaspProjectDir, srcDirInWaspProjectDir, extPublicDirInWaspProjectDir, - tsconfigInWaspProjectDir, prismaSchemaFileInWaspProjectDir, - tsConfigInWaspProjectDir, + getSrcTsConfigInWaspProjectDir, + srcTsConfigInWaspLangProject, + srcTsConfigInWaspTsProject, ) where @@ -41,7 +45,15 @@ data DotWaspDir -- Here we put everything that wasp generates. data PackageJsonFile -data TsConfigFile +data SrcTsConfigFile + +data WaspFilePath + = WaspLang !(Path' Abs (File WaspLangFile)) + | WaspTs !(Path' Abs (File WaspTsFile)) + +data WaspLangFile + +data WaspTsFile -- | NOTE: If you change the depth of this path, also update @waspProjectDirFromProjectRootDir@ below. -- TODO: SHould this be renamed to include word "root"? @@ -76,8 +88,17 @@ dotWaspInfoFileInGeneratedCodeDir = [relfile|.waspinfo|] packageJsonInWaspProjectDir :: Path' (Rel WaspProjectDir) (File PackageJsonFile) packageJsonInWaspProjectDir = [relfile|package.json|] -tsConfigInWaspProjectDir :: Path' (Rel WaspProjectDir) (File TsConfigFile) -tsConfigInWaspProjectDir = [relfile|tsconfig.json|] +-- TODO: The entire tsconfig story is very fragile +getSrcTsConfigInWaspProjectDir :: WaspFilePath -> Path' (Rel WaspProjectDir) (File SrcTsConfigFile) +getSrcTsConfigInWaspProjectDir = \case + WaspTs _ -> srcTsConfigInWaspTsProject + WaspLang _ -> srcTsConfigInWaspLangProject + +srcTsConfigInWaspLangProject :: Path' (Rel WaspProjectDir) (File SrcTsConfigFile) +srcTsConfigInWaspLangProject = [relfile|tsconfig.json|] + +srcTsConfigInWaspTsProject :: Path' (Rel WaspProjectDir) (File SrcTsConfigFile) +srcTsConfigInWaspTsProject = [relfile|tsconfig.src.json|] packageLockJsonInWaspProjectDir :: Path' (Rel WaspProjectDir) File' packageLockJsonInWaspProjectDir = [relfile|package-lock.json|] @@ -91,13 +112,10 @@ srcDirInWaspProjectDir = [reldir|src|] extPublicDirInWaspProjectDir :: Path' (Rel WaspProjectDir) (Dir SourceExternalPublicDir) extPublicDirInWaspProjectDir = [reldir|public|] -tsconfigInWaspProjectDir :: Path' (Rel WaspProjectDir) File' -tsconfigInWaspProjectDir = [relfile|tsconfig.json|] - findFileInWaspProjectDir :: Path' Abs (Dir WaspProjectDir) -> - Path' (Rel WaspProjectDir) (File file) -> - IO (Maybe (Path' Abs (File file))) + Path' (Rel WaspProjectDir) (File f) -> + IO (Maybe (Path' Abs (File f))) findFileInWaspProjectDir waspDir file = do let fileAbsFp = waspDir file fileExists <- doesFileExist $ toFilePath fileAbsFp diff --git a/waspc/src/Wasp/Project/Deployment.hs b/waspc/src/Wasp/Project/Deployment.hs index 91543af364..9706f13ccf 100644 --- a/waspc/src/Wasp/Project/Deployment.hs +++ b/waspc/src/Wasp/Project/Deployment.hs @@ -11,7 +11,7 @@ import StrongPath (Abs, Dir, Path', relfile, toFilePath, ()) import System.Directory (doesFileExist) import System.Exit (ExitCode (..)) import qualified System.Process as P -import Wasp.NodePackageFFI (Package (DeployPackage), getPackageProcessOptions) +import Wasp.NodePackageFFI (RunnablePackage (DeployPackage), getPackageProcessOptions) import Wasp.Project.Common (WaspProjectDir) loadUserDockerfileContents :: Path' Abs (Dir WaspProjectDir) -> IO (Maybe Text) diff --git a/waspc/src/Wasp/Project/ExternalConfig.hs b/waspc/src/Wasp/Project/ExternalConfig.hs index c03a2732de..742d67285d 100644 --- a/waspc/src/Wasp/Project/ExternalConfig.hs +++ b/waspc/src/Wasp/Project/ExternalConfig.hs @@ -5,15 +5,16 @@ module Wasp.Project.ExternalConfig where import Control.Monad.Except (ExceptT (ExceptT), runExceptT) -import StrongPath (Abs, Dir, Path') +import StrongPath (Abs, Dir, File, Path', Rel) import qualified Wasp.ExternalConfig.PackageJson as P import qualified Wasp.ExternalConfig.TsConfig as T import Wasp.Project.Common ( CompileError, + SrcTsConfigFile, WaspProjectDir, ) import Wasp.Project.ExternalConfig.PackageJson (analyzePackageJsonFile) -import Wasp.Project.ExternalConfig.TsConfig (analyzeTsConfigFile) +import Wasp.Project.ExternalConfig.TsConfig (analyzeSrcTsConfigFile) data ExternalConfigs = ExternalConfigs { _packageJson :: P.PackageJson, @@ -21,10 +22,13 @@ data ExternalConfigs = ExternalConfigs } deriving (Show) -analyzeExternalConfigs :: Path' Abs (Dir WaspProjectDir) -> IO (Either [CompileError] ExternalConfigs) -analyzeExternalConfigs waspDir = runExceptT $ do +analyzeExternalConfigs :: + Path' Abs (Dir WaspProjectDir) -> + Path' (Rel WaspProjectDir) (File SrcTsConfigFile) -> + IO (Either [CompileError] ExternalConfigs) +analyzeExternalConfigs waspDir srcTsConfigFile = runExceptT $ do packageJsonContent <- ExceptT $ analyzePackageJsonFile waspDir - tsConfigContent <- ExceptT $ analyzeTsConfigFile waspDir + tsConfigContent <- ExceptT $ analyzeSrcTsConfigFile waspDir srcTsConfigFile return $ ExternalConfigs diff --git a/waspc/src/Wasp/Project/ExternalConfig/TsConfig.hs b/waspc/src/Wasp/Project/ExternalConfig/TsConfig.hs index 8693241a9b..e12ca1e591 100644 --- a/waspc/src/Wasp/Project/ExternalConfig/TsConfig.hs +++ b/waspc/src/Wasp/Project/ExternalConfig/TsConfig.hs @@ -1,43 +1,40 @@ module Wasp.Project.ExternalConfig.TsConfig - ( analyzeTsConfigFile, + ( analyzeSrcTsConfigFile, ) where +import Control.Arrow (left) import Control.Monad.Except (ExceptT (ExceptT), runExceptT, throwError) import qualified Data.ByteString.Lazy.UTF8 as BS import Data.Either.Extra (maybeToEither) -import StrongPath (Abs, Dir, File, Path', toFilePath) +import StrongPath (Abs, Dir, File, Path', Rel, toFilePath) import qualified Wasp.ExternalConfig.TsConfig as T -import Wasp.Generator.ExternalConfig.TsConfig (validateTsConfig) +import Wasp.Generator.ExternalConfig.TsConfig (validateSrcTsConfig) import Wasp.Project.Common - ( TsConfigFile, + ( SrcTsConfigFile, WaspProjectDir, findFileInWaspProjectDir, - tsConfigInWaspProjectDir, ) import qualified Wasp.Util.IO as IOUtil import Wasp.Util.Json (parseJsonWithComments) -analyzeTsConfigFile :: Path' Abs (Dir WaspProjectDir) -> IO (Either [String] T.TsConfig) -analyzeTsConfigFile waspDir = runExceptT $ do - tsConfigFile <- ExceptT findTsConfigOrError - tsConfig <- ExceptT $ readTsConfigFile tsConfigFile - case validateTsConfig tsConfig of - [] -> return tsConfig +analyzeSrcTsConfigFile :: + Path' Abs (Dir WaspProjectDir) -> + Path' (Rel WaspProjectDir) (File SrcTsConfigFile) -> + IO (Either [String] T.TsConfig) +analyzeSrcTsConfigFile waspDir srcTsConfigFile = runExceptT $ do + tsConfigFileContents <- ExceptT findTsConfigOrError + srcTsConfigContents <- ExceptT $ left (: []) <$> readTsConfigFile tsConfigFileContents + case validateSrcTsConfig srcTsConfigContents of + [] -> return srcTsConfigContents errors -> throwError errors where - findTsConfigOrError = maybeToEither [fileNotFoundMessage] <$> findTsConfigFile waspDir + findTsConfigOrError = maybeToEither [fileNotFoundMessage] <$> findFileInWaspProjectDir waspDir srcTsConfigFile fileNotFoundMessage = "Couldn't find the tsconfig.json file in the " ++ toFilePath waspDir ++ " directory" -findTsConfigFile :: Path' Abs (Dir WaspProjectDir) -> IO (Maybe (Path' Abs (File TsConfigFile))) -findTsConfigFile waspProjectDir = findFileInWaspProjectDir waspProjectDir tsConfigInWaspProjectDir - -readTsConfigFile :: Path' Abs (File TsConfigFile) -> IO (Either [String] T.TsConfig) +-- TODO: Reduce polymorphism, should only work with TsConfig files +readTsConfigFile :: Path' Abs (File f) -> IO (Either String T.TsConfig) readTsConfigFile tsConfigFile = do tsConfigContent <- IOUtil.readFileBytes tsConfigFile - parseResult <- parseJsonWithComments . BS.toString $ tsConfigContent - - case parseResult of - Right tsConfig -> return $ Right tsConfig - Left err -> return $ Left ["Failed to parse tsconfig.json file: " ++ err] + return $ left ("Failed to parse tsconfig file" ++) parseResult diff --git a/waspc/src/Wasp/Project/Studio.hs b/waspc/src/Wasp/Project/Studio.hs index 53ea4bd43e..96d9e5c10f 100644 --- a/waspc/src/Wasp/Project/Studio.hs +++ b/waspc/src/Wasp/Project/Studio.hs @@ -5,7 +5,7 @@ where import System.Exit (ExitCode (..)) import qualified System.Process as P -import Wasp.NodePackageFFI (Package (WaspStudioPackage), getPackageProcessOptions) +import Wasp.NodePackageFFI (RunnablePackage (WaspStudioPackage), getPackageProcessOptions) startStudio :: -- | Path to the data JSON file. diff --git a/waspc/src/Wasp/Psl/Ast/Model.hs b/waspc/src/Wasp/Psl/Ast/Model.hs index 7eb9cc82d9..e9c6e336a8 100644 --- a/waspc/src/Wasp/Psl/Ast/Model.hs +++ b/waspc/src/Wasp/Psl/Ast/Model.hs @@ -7,7 +7,7 @@ module Wasp.Psl.Ast.Model Field (..), FieldType (..), FieldTypeModifier (..), - getFields, + getName, ) where @@ -60,5 +60,5 @@ data FieldTypeModifier | Optional deriving (Show, Eq, Data) -getFields :: Model -> [Field] -getFields (Model _ (Body elements)) = [field | ElementField field <- elements] +getName :: Model -> Name +getName (Model name _) = name diff --git a/waspc/src/Wasp/Psl/Ast/Schema.hs b/waspc/src/Wasp/Psl/Ast/Schema.hs index df551c9ea8..403b4452dc 100644 --- a/waspc/src/Wasp/Psl/Ast/Schema.hs +++ b/waspc/src/Wasp/Psl/Ast/Schema.hs @@ -7,6 +7,7 @@ module Wasp.Psl.Ast.Schema getEnums, getDatasources, getGenerators, + getModelNames, ) where @@ -14,6 +15,7 @@ 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) @@ -49,3 +51,6 @@ 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/TypeScript/Inspect/Exports.hs b/waspc/src/Wasp/TypeScript/Inspect/Exports.hs index 661491a587..09c9ec935c 100644 --- a/waspc/src/Wasp/TypeScript/Inspect/Exports.hs +++ b/waspc/src/Wasp/TypeScript/Inspect/Exports.hs @@ -28,7 +28,7 @@ import qualified System.Process as P import Wasp.Analyzer (SourcePosition) import Wasp.Analyzer.Parser.SourcePosition (SourcePosition (SourcePosition)) import Wasp.Analyzer.Parser.SourceRegion (SourceRegion (SourceRegion)) -import Wasp.NodePackageFFI (Package (TsInspectPackage), getPackageProcessOptions) +import Wasp.NodePackageFFI (RunnablePackage (TsInspectPackage), getPackageProcessOptions) -- | Attempt to get list of exported names from TypeScript files. -- diff --git a/waspc/src/Wasp/Util/Aeson.hs b/waspc/src/Wasp/Util/Aeson.hs index e243467809..488417528d 100644 --- a/waspc/src/Wasp/Util/Aeson.hs +++ b/waspc/src/Wasp/Util/Aeson.hs @@ -1,10 +1,11 @@ module Wasp.Util.Aeson ( encodeToText, decodeFromString, + encodeToString, ) where -import Data.Aeson (FromJSON, ToJSON, eitherDecode) +import Data.Aeson (FromJSON, ToJSON, eitherDecode, encode) import Data.Aeson.Text (encodeToTextBuilder) import qualified Data.ByteString.Lazy.UTF8 as BS import Data.Text (Text) @@ -16,3 +17,6 @@ encodeToText = toStrict . toLazyText . encodeToTextBuilder decodeFromString :: FromJSON a => String -> Either String a decodeFromString = eitherDecode . BS.fromString + +encodeToString :: ToJSON a => a -> String +encodeToString = BS.toString . encode diff --git a/waspc/test/AppSpec/FromJSONTest.hs b/waspc/test/AppSpec/FromJSONTest.hs index a9f3247135..11e7130db2 100644 --- a/waspc/test/AppSpec/FromJSONTest.hs +++ b/waspc/test/AppSpec/FromJSONTest.hs @@ -19,6 +19,7 @@ import Wasp.AppSpec.Page (Page) import qualified Wasp.AppSpec.Page as Page import Wasp.AppSpec.Query (Query) import qualified Wasp.AppSpec.Query as Query +import qualified Wasp.AppSpec.Route as Route spec_AppSpecFromJSON :: Spec spec_AppSpecFromJSON = do @@ -81,6 +82,21 @@ spec_AppSpecFromJSON = do } |] `shouldDecodeTo` (Nothing :: Maybe Page.Page) + describe "Route" $ do + it "parses a valid Route JSON" $ do + [trimming| + { + "path": "/foo", + "to": ${pageRef} + } + |] + `shouldDecodeTo` Just + ( Route.Route + { Route.path = "/foo", + Route.to = fromJust $ decodeJson pageRef + } + ) + describe "Ref" $ do it "parses a valid Entity Ref JSON" $ do fooEntityRef `shouldDecodeTo` Just (Ref.Ref "foo" :: Ref Entity) @@ -94,16 +110,6 @@ spec_AppSpecFromJSON = do } |] `shouldDecodeTo` Just (Ref.Ref "foo" :: Ref Query) - it "fails to parse an invalid declType" $ do - [trimming| - { - "name": "foo", - "declType": "IMadeThisUp" - } - |] - -- NOTE: We are using `Ref Entity` here, because the Show instance in - -- shouldDecodeTo demands a proper type. - `shouldDecodeTo` (Nothing :: Maybe (Ref Entity)) describe "Query" $ do it "parses a valid Query JSON with auth and entities" $ do [trimming| @@ -288,8 +294,8 @@ spec_AppSpecFromJSON = do } ) where - extNamedImportJson = [trimming| { "kind": "named", "name" : "foo", "path": "folder/file.js" }|] - extDefaultImportJson = [trimming| { "kind": "default", "name" : "foo", "path": "folder/subfolder/file.js" }|] + extNamedImportJson = [trimming| { "kind": "named", "name" : "foo", "path": "@src/folder/file.js" }|] + extDefaultImportJson = [trimming| { "kind": "default", "name" : "foo", "path": "@src/folder/subfolder/file.js" }|] fooEntityRef = [trimming| { "name": "foo", "declType": "Entity" }|] barEntityRef = [trimming| { "name": "bar", "declType": "Entity" }|] diff --git a/waspc/waspc.cabal b/waspc/waspc.cabal index 058aca0162..be15e43c26 100644 --- a/waspc/waspc.cabal +++ b/waspc/waspc.cabal @@ -464,7 +464,6 @@ library waspls Wasp.LSP.TypeInference Wasp.LSP.Util Wasp.LSP.Prisma.Analyze - Wasp.LSP.Prisma.Util build-depends: base , aeson @@ -565,6 +564,7 @@ library cli-lib Wasp.Cli.Command.Telemetry.Project Wasp.Cli.Command.Telemetry.User Wasp.Cli.Command.Test + Wasp.Cli.Command.TsConfigSetup Wasp.Cli.Command.Watch Wasp.Cli.Command.WaspLS Wasp.Cli.Common diff --git a/waspc/waspls/src/Wasp/LSP/ExtImport/ExportsCache.hs b/waspc/waspls/src/Wasp/LSP/ExtImport/ExportsCache.hs index dba605eca6..369346be39 100644 --- a/waspc/waspls/src/Wasp/LSP/ExtImport/ExportsCache.hs +++ b/waspc/waspls/src/Wasp/LSP/ExtImport/ExportsCache.hs @@ -22,7 +22,7 @@ import Wasp.LSP.ExtImport.Path (WaspStyleExtFilePath, absPathToCachePath, cacheP import Wasp.LSP.ExtImport.Syntax (ExtImportNode (einName, einPath), getAllExtImports) import Wasp.LSP.ServerMonads (HandlerM, ServerM, getProjectRootDir, handler, modify) import qualified Wasp.LSP.ServerState as State -import Wasp.Project.Common (tsconfigInWaspProjectDir) +import Wasp.Project.Common (srcTsConfigInWaspLangProject) import qualified Wasp.TypeScript.Inspect.Exports as TS -- | Based on the files imported in the external imports of the current concrete @@ -63,7 +63,7 @@ refreshExportsOfFiles files = do getExportRequestForFile projectRootDir file = TS.TsExportsRequest { TS.filepaths = [SP.fromAbsFile file], - TS.tsconfig = Just $ SP.fromAbsFile $ projectRootDir tsconfigInWaspProjectDir + TS.tsconfig = Just $ SP.fromAbsFile $ projectRootDir srcTsConfigInWaspLangProject } -- Replaces entries in the exports cache with the exports lists in the diff --git a/waspc/waspls/src/Wasp/LSP/Prisma/Analyze.hs b/waspc/waspls/src/Wasp/LSP/Prisma/Analyze.hs index d8ec5caf46..0ec4b7565d 100644 --- a/waspc/waspls/src/Wasp/LSP/Prisma/Analyze.hs +++ b/waspc/waspls/src/Wasp/LSP/Prisma/Analyze.hs @@ -4,11 +4,11 @@ import Control.Lens ((.~)) import Control.Monad.Cont (liftIO) import Control.Monad.Log.Class (logM) import StrongPath (Abs, Dir, Path') -import Wasp.LSP.Prisma.Util (showModelNames) 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) analyzeAndSetPrismaSchema :: Path' Abs (Dir WaspProjectDir) -> ServerM () analyzeAndSetPrismaSchema waspDir = do @@ -18,7 +18,7 @@ analyzeAndSetPrismaSchema waspDir = do logOutput "warnings" $ show warnings (Right prismaSchemaAst, warnings) -> do logOutput "warnings" $ show warnings - logOutput "models" $ showModelNames prismaSchemaAst + logOutput "models" $ show $ getModelNames prismaSchemaAst modify (State.prismaSchemaAst .~ prismaSchemaAst) where logOutput :: String -> String -> ServerM () diff --git a/waspc/waspls/src/Wasp/LSP/Prisma/Util.hs b/waspc/waspls/src/Wasp/LSP/Prisma/Util.hs deleted file mode 100644 index 6dbe983765..0000000000 --- a/waspc/waspls/src/Wasp/LSP/Prisma/Util.hs +++ /dev/null @@ -1,13 +0,0 @@ -module Wasp.LSP.Prisma.Util - ( showModelNames, - ) -where - -import qualified Wasp.Psl.Ast.Model as Psl.Model -import qualified Wasp.Psl.Ast.Schema as Psl.Schema - -showModelNames :: Psl.Schema.Schema -> String -showModelNames = unwords . getModelNames - -getModelNames :: Psl.Schema.Schema -> [String] -getModelNames = fmap (\(Psl.Model.Model name _) -> name) . Psl.Schema.getModels