├── .config └── dotnet-tools.json ├── .devcontainer ├── devcontainer.json └── init.sh ├── .editorconfig ├── .gitattributes ├── .github └── workflows │ ├── main.yml │ └── pr.yml ├── .gitignore ├── CHANGELOG.md ├── Directory.Build.props ├── Directory.Packages.props ├── Fable.Daemon.Tests ├── DebugTests.fs ├── Fable.Daemon.Tests.fsproj └── Program.fs ├── Fable.Daemon ├── Caching.fs ├── CoolCatCracking.fs ├── Debug.fs ├── Debug.fsi ├── Fable.Daemon.fsproj ├── MSBuild.fs ├── Program.fs ├── README.md ├── Types.fs └── debug │ ├── favicon.ico │ └── index.html ├── LICENSE ├── README.md ├── bun.lock ├── changelog-updater.js ├── cracking.fsx ├── docs ├── _body.html ├── _head.html ├── content │ └── fsdocs-theme.css ├── debug.md ├── getting-started.md ├── how.md ├── img │ ├── debug-tool.png │ ├── favicon.ico │ └── logo.png ├── implications.md ├── index.md ├── local-fable-compiler.md ├── recipes.md ├── scripts │ └── command.js └── status.md ├── global.json ├── ideas.md ├── index.js ├── package.json ├── sample-project ├── .gitignore ├── App.fsproj ├── Component.fs ├── Component.fsi ├── Directory.Build.props ├── Library.fs ├── Math.fs ├── README.md ├── app.css ├── bun.lock ├── index.html ├── package.json ├── public │ └── vite.svg └── vite.config.js ├── tsconfig.json ├── types.d.ts └── vite-plugin-fable.sln /.config/dotnet-tools.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "isRoot": true, 4 | "tools": { 5 | "fantomas": { 6 | "version": "7.0.0", 7 | "commands": [ 8 | "fantomas" 9 | ], 10 | "rollForward": false 11 | }, 12 | "fsdocs-tool": { 13 | "version": "20.0.0", 14 | "commands": [ 15 | "fsdocs" 16 | ], 17 | "rollForward": false 18 | }, 19 | "dotnet-outdated-tool": { 20 | "version": "4.6.8", 21 | "commands": [ 22 | "dotnet-outdated" 23 | ], 24 | "rollForward": false 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the 2 | // README at: https://github.com/devcontainers/templates/tree/main/src/dotnet-fsharp 3 | { 4 | "name": "vite-plugin-fable", 5 | "image": "mcr.microsoft.com/dotnet/sdk:8.0", 6 | // Configure tool-specific properties. 7 | "customizations": { 8 | // Configure properties specific to VS Code. 9 | "vscode": { 10 | // Add the IDs of extensions you want installed when the container is created. 11 | "extensions": ["Ionide.Ionide-fsharp@7.20.0", "ms-vscode.csharp"], 12 | "settings": { 13 | "FSharp.useSdkScripts": true, 14 | "FSharp.fsac.netCoreDllPath": "/root/.vscode-server/extensions/ionide.ionide-fsharp-7.20.0/bin/net7.0/fsautocomplete.dll" 15 | } 16 | } 17 | }, 18 | // Features to add to the dev container. More info: https://containers.dev/features. 19 | "features": { 20 | "ghcr.io/devcontainers/features/common-utils:2.4.2": {}, 21 | "ghcr.io/devcontainers/features/git:1.2.0": {}, 22 | "ghcr.io/devcontainers/features/github-cli:1.0.11": {}, 23 | "ghcr.io/devcontainers/features/dotnet:2.0.5": {}, 24 | "ghcr.io/michidk/devcontainers-features/bun:1": {}, 25 | "ghcr.io/devcontainers/features/node:1": { 26 | "version": "22" 27 | }, 28 | "ghcr.io/devcontainers/features/powershell:1": {} 29 | }, 30 | "postCreateCommand": ".devcontainer/init.sh", 31 | 32 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 33 | "forwardPorts": ["4000:4000"] 34 | 35 | // Use 'postCreateCommand' to run commands after the container is created. 36 | // "postCreateCommand": "dotnet restore", 37 | 38 | // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. 39 | // "remoteUser": "root" 40 | } 41 | -------------------------------------------------------------------------------- /.devcontainer/init.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | dotnet tool restore 3 | dotnet restore 4 | bun i -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | 6 | [*.{fsproj,props,js}] 7 | indent_size = 2 8 | 9 | [*.{fs,fsi,fsx}] 10 | fsharp_space_before_uppercase_invocation = true 11 | fsharp_space_before_member = true 12 | fsharp_space_before_colon = true 13 | fsharp_space_before_semicolon = true 14 | fsharp_multiline_bracket_style = aligned 15 | fsharp_newline_between_type_definition_and_members = true 16 | fsharp_align_function_signature_to_indentation = true 17 | fsharp_alternative_long_member_definitions = true 18 | fsharp_multi_line_lambda_closing_newline = true 19 | fsharp_experimental_keep_indent_in_branch = true 20 | fsharp_bar_before_discriminated_union_declaration = true 21 | 22 | [Fable.Daemon/Debug.fs] 23 | fsharp_experimental_elmish=true 24 | 25 | [sample-project/Component.fs] 26 | fsharp_experimental_elmish=true -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Automatically normalize line endings 2 | * text=auto 3 | 4 | # Always use lf for source files 5 | *.fs text eol=lf 6 | *.fsx text eol=lf 7 | *.fsi text eol=lf 8 | *.md text eol=lf 9 | *.js text eol=lf 10 | *.json text eol=lf -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Main 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | 7 | permissions: 8 | contents: read 9 | pages: write 10 | id-token: write 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | with: 18 | fetch-depth: 0 19 | 20 | - uses: oven-sh/setup-bun@v1 21 | 22 | - name: Setup dotnet 23 | uses: actions/setup-dotnet@v4 24 | 25 | - name: Restore tools 26 | run: dotnet tool restore 27 | 28 | - name: Restore solution 29 | run: dotnet restore 30 | 31 | - name: Install node_modules 32 | run: bun install --frozen-lockfile 33 | 34 | - name: TypeScript check 35 | run: bun run lint 36 | 37 | - name: Build daemon 38 | run : bun run postinstall 39 | 40 | - name: Build docs 41 | run: dotnet fsdocs build --noapidocs --projects "$(pwd)/Fable.Daemon/Fable.Daemon.fsproj" 42 | 43 | - name: Upload artifact 44 | uses: actions/upload-pages-artifact@v3 45 | with: 46 | path: ./output 47 | 48 | deploy: 49 | environment: 50 | name: github-pages 51 | url: ${{ steps.deployment.outputs.page_url }} 52 | runs-on: ubuntu-latest 53 | needs: build 54 | steps: 55 | - name: Deploy to GitHub Pages 56 | id: deployment 57 | uses: actions/deploy-pages@v4 58 | -------------------------------------------------------------------------------- /.github/workflows/pr.yml: -------------------------------------------------------------------------------- 1 | name: PR 2 | 3 | on: [ pull_request ] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v4 10 | with: 11 | fetch-depth: 0 12 | 13 | - uses: oven-sh/setup-bun@v1 14 | 15 | - name: Setup dotnet 16 | uses: actions/setup-dotnet@v4 17 | 18 | - name: Restore tools 19 | run: dotnet tool restore 20 | 21 | - name: Restore solution 22 | run: dotnet restore 23 | 24 | - name: Install node_modules 25 | run: bun install --frozen-lockfile 26 | 27 | - name: TypeScript check 28 | run: bun run lint 29 | 30 | - name: Build daemon 31 | run : bun run postinstall 32 | 33 | - name: Build sample 34 | run: bun i && bun run build 2>&1 | grep -i "error" && exit 1 || true 35 | working-directory: ./sample-project 36 | 37 | - name: Build docs 38 | run: dotnet fsdocs build --noapidocs --projects "$(pwd)/Fable.Daemon/Fable.Daemon.fsproj" 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from `dotnet new gitignore` 5 | 6 | # dotenv files 7 | .env 8 | 9 | # User-specific files 10 | *.rsuser 11 | *.suo 12 | *.user 13 | *.userosscache 14 | *.sln.docstates 15 | 16 | # User-specific files (MonoDevelop/Xamarin Studio) 17 | *.userprefs 18 | 19 | # Mono auto generated files 20 | mono_crash.* 21 | 22 | # Build results 23 | [Dd]ebug/ 24 | [Dd]ebugPublic/ 25 | [Rr]elease/ 26 | [Rr]eleases/ 27 | x64/ 28 | x86/ 29 | [Ww][Ii][Nn]32/ 30 | [Aa][Rr][Mm]/ 31 | [Aa][Rr][Mm]64/ 32 | bld/ 33 | [Bb]in/ 34 | [Oo]bj/ 35 | [Ll]og/ 36 | [Ll]ogs/ 37 | 38 | # Visual Studio 2015/2017 cache/options directory 39 | .vs/ 40 | # Uncomment if you have tasks that create the project's static files in wwwroot 41 | #wwwroot/ 42 | 43 | # Visual Studio 2017 auto generated files 44 | Generated\ Files/ 45 | 46 | # MSTest test Results 47 | [Tt]est[Rr]esult*/ 48 | [Bb]uild[Ll]og.* 49 | 50 | # NUnit 51 | *.VisualState.xml 52 | TestResult.xml 53 | nunit-*.xml 54 | 55 | # Build Results of an ATL Project 56 | [Dd]ebugPS/ 57 | [Rr]eleasePS/ 58 | dlldata.c 59 | 60 | # Benchmark Results 61 | BenchmarkDotNet.Artifacts/ 62 | 63 | # .NET 64 | project.lock.json 65 | project.fragment.lock.json 66 | artifacts/ 67 | 68 | # Tye 69 | .tye/ 70 | 71 | # ASP.NET Scaffolding 72 | ScaffoldingReadMe.txt 73 | 74 | # StyleCop 75 | StyleCopReport.xml 76 | 77 | # Files built by Visual Studio 78 | *_i.c 79 | *_p.c 80 | *_h.h 81 | *.ilk 82 | *.meta 83 | *.obj 84 | *.iobj 85 | *.pch 86 | *.pdb 87 | *.ipdb 88 | *.pgc 89 | *.pgd 90 | *.rsp 91 | *.sbr 92 | *.tlb 93 | *.tli 94 | *.tlh 95 | *.tmp 96 | *.tmp_proj 97 | *_wpftmp.csproj 98 | *.log 99 | *.tlog 100 | *.vspscc 101 | *.vssscc 102 | .builds 103 | *.pidb 104 | *.svclog 105 | *.scc 106 | 107 | # Chutzpah Test files 108 | _Chutzpah* 109 | 110 | # Visual C++ cache files 111 | ipch/ 112 | *.aps 113 | *.ncb 114 | *.opendb 115 | *.opensdf 116 | *.sdf 117 | *.cachefile 118 | *.VC.db 119 | *.VC.VC.opendb 120 | 121 | # Visual Studio profiler 122 | *.psess 123 | *.vsp 124 | *.vspx 125 | *.sap 126 | 127 | # Visual Studio Trace Files 128 | *.e2e 129 | 130 | # TFS 2012 Local Workspace 131 | $tf/ 132 | 133 | # Guidance Automation Toolkit 134 | *.gpState 135 | 136 | # ReSharper is a .NET coding add-in 137 | _ReSharper*/ 138 | *.[Rr]e[Ss]harper 139 | *.DotSettings.user 140 | 141 | # TeamCity is a build add-in 142 | _TeamCity* 143 | 144 | # DotCover is a Code Coverage Tool 145 | *.dotCover 146 | 147 | # AxoCover is a Code Coverage Tool 148 | .axoCover/* 149 | !.axoCover/settings.json 150 | 151 | # Coverlet is a free, cross platform Code Coverage Tool 152 | coverage*.json 153 | coverage*.xml 154 | coverage*.info 155 | 156 | # Visual Studio code coverage results 157 | *.coverage 158 | *.coveragexml 159 | 160 | # NCrunch 161 | _NCrunch_* 162 | .*crunch*.local.xml 163 | nCrunchTemp_* 164 | 165 | # MightyMoose 166 | *.mm.* 167 | AutoTest.Net/ 168 | 169 | # Web workbench (sass) 170 | .sass-cache/ 171 | 172 | # Installshield output folder 173 | [Ee]xpress/ 174 | 175 | # DocProject is a documentation generator add-in 176 | DocProject/buildhelp/ 177 | DocProject/Help/*.HxT 178 | DocProject/Help/*.HxC 179 | DocProject/Help/*.hhc 180 | DocProject/Help/*.hhk 181 | DocProject/Help/*.hhp 182 | DocProject/Help/Html2 183 | DocProject/Help/html 184 | 185 | # Click-Once directory 186 | publish/ 187 | 188 | # Publish Web Output 189 | *.[Pp]ublish.xml 190 | *.azurePubxml 191 | # Note: Comment the next line if you want to checkin your web deploy settings, 192 | # but database connection strings (with potential passwords) will be unencrypted 193 | *.pubxml 194 | *.publishproj 195 | 196 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 197 | # checkin your Azure Web App publish settings, but sensitive information contained 198 | # in these scripts will be unencrypted 199 | PublishScripts/ 200 | 201 | # NuGet Packages 202 | *.nupkg 203 | # NuGet Symbol Packages 204 | *.snupkg 205 | # The packages folder can be ignored because of Package Restore 206 | **/[Pp]ackages/* 207 | # except build/, which is used as an MSBuild target. 208 | !**/[Pp]ackages/build/ 209 | # Uncomment if necessary however generally it will be regenerated when needed 210 | #!**/[Pp]ackages/repositories.config 211 | # NuGet v3's project.json files produces more ignorable files 212 | *.nuget.props 213 | *.nuget.targets 214 | 215 | # Microsoft Azure Build Output 216 | csx/ 217 | *.build.csdef 218 | 219 | # Microsoft Azure Emulator 220 | ecf/ 221 | rcf/ 222 | 223 | # Windows Store app package directories and files 224 | AppPackages/ 225 | BundleArtifacts/ 226 | Package.StoreAssociation.xml 227 | _pkginfo.txt 228 | *.appx 229 | *.appxbundle 230 | *.appxupload 231 | 232 | # Visual Studio cache files 233 | # files ending in .cache can be ignored 234 | *.[Cc]ache 235 | # but keep track of directories ending in .cache 236 | !?*.[Cc]ache/ 237 | 238 | # Others 239 | ClientBin/ 240 | ~$* 241 | *~ 242 | *.dbmdl 243 | *.dbproj.schemaview 244 | *.jfm 245 | *.pfx 246 | *.publishsettings 247 | orleans.codegen.cs 248 | 249 | # Including strong name files can present a security risk 250 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 251 | #*.snk 252 | 253 | # Since there are multiple workflows, uncomment next line to ignore bower_components 254 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 255 | #bower_components/ 256 | 257 | # RIA/Silverlight projects 258 | Generated_Code/ 259 | 260 | # Backup & report files from converting an old project file 261 | # to a newer Visual Studio version. Backup files are not needed, 262 | # because we have git ;-) 263 | _UpgradeReport_Files/ 264 | Backup*/ 265 | UpgradeLog*.XML 266 | UpgradeLog*.htm 267 | ServiceFabricBackup/ 268 | *.rptproj.bak 269 | 270 | # SQL Server files 271 | *.mdf 272 | *.ldf 273 | *.ndf 274 | 275 | # Business Intelligence projects 276 | *.rdl.data 277 | *.bim.layout 278 | *.bim_*.settings 279 | *.rptproj.rsuser 280 | *- [Bb]ackup.rdl 281 | *- [Bb]ackup ([0-9]).rdl 282 | *- [Bb]ackup ([0-9][0-9]).rdl 283 | 284 | # Microsoft Fakes 285 | FakesAssemblies/ 286 | 287 | # GhostDoc plugin setting file 288 | *.GhostDoc.xml 289 | 290 | # Node.js Tools for Visual Studio 291 | .ntvs_analysis.dat 292 | node_modules/ 293 | 294 | # Visual Studio 6 build log 295 | *.plg 296 | 297 | # Visual Studio 6 workspace options file 298 | *.opt 299 | 300 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 301 | *.vbw 302 | 303 | # Visual Studio 6 auto-generated project file (contains which files were open etc.) 304 | *.vbp 305 | 306 | # Visual Studio 6 workspace and project file (working project files containing files to include in project) 307 | *.dsw 308 | *.dsp 309 | 310 | # Visual Studio 6 technical files 311 | *.ncb 312 | *.aps 313 | 314 | # Visual Studio LightSwitch build output 315 | **/*.HTMLClient/GeneratedArtifacts 316 | **/*.DesktopClient/GeneratedArtifacts 317 | **/*.DesktopClient/ModelManifest.xml 318 | **/*.Server/GeneratedArtifacts 319 | **/*.Server/ModelManifest.xml 320 | _Pvt_Extensions 321 | 322 | # Paket dependency manager 323 | .paket/paket.exe 324 | paket-files/ 325 | 326 | # FAKE - F# Make 327 | .fake/ 328 | 329 | # CodeRush personal settings 330 | .cr/personal 331 | 332 | # Python Tools for Visual Studio (PTVS) 333 | __pycache__/ 334 | *.pyc 335 | 336 | # Cake - Uncomment if you are using it 337 | # tools/** 338 | # !tools/packages.config 339 | 340 | # Tabs Studio 341 | *.tss 342 | 343 | # Telerik's JustMock configuration file 344 | *.jmconfig 345 | 346 | # BizTalk build output 347 | *.btp.cs 348 | *.btm.cs 349 | *.odx.cs 350 | *.xsd.cs 351 | 352 | # OpenCover UI analysis results 353 | OpenCover/ 354 | 355 | # Azure Stream Analytics local run output 356 | ASALocalRun/ 357 | 358 | # MSBuild Binary and Structured Log 359 | *.binlog 360 | 361 | # NVidia Nsight GPU debugger configuration file 362 | *.nvuser 363 | 364 | # MFractors (Xamarin productivity tool) working folder 365 | .mfractor/ 366 | 367 | # Local History for Visual Studio 368 | .localhistory/ 369 | 370 | # Visual Studio History (VSHistory) files 371 | .vshistory/ 372 | 373 | # BeatPulse healthcheck temp database 374 | healthchecksdb 375 | 376 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 377 | MigrationBackup/ 378 | 379 | # Ionide (cross platform F# VS Code tools) working folder 380 | .ionide/ 381 | 382 | # Fody - auto-generated XML schema 383 | FodyWeavers.xsd 384 | 385 | # VS Code files for those working on multiple tools 386 | .vscode/* 387 | !.vscode/settings.json 388 | !.vscode/tasks.json 389 | !.vscode/launch.json 390 | !.vscode/extensions.json 391 | *.code-workspace 392 | 393 | # Local History for Visual Studio Code 394 | .history/ 395 | 396 | # Windows Installer files from build outputs 397 | *.cab 398 | *.msi 399 | *.msix 400 | *.msm 401 | *.msp 402 | 403 | # JetBrains Rider 404 | *.sln.iml 405 | .idea 406 | 407 | ## 408 | ## Visual studio for Mac 409 | ## 410 | 411 | 412 | # globs 413 | Makefile.in 414 | *.userprefs 415 | *.usertasks 416 | config.make 417 | config.status 418 | aclocal.m4 419 | install-sh 420 | autom4te.cache/ 421 | *.tar.gz 422 | tarballs/ 423 | test-results/ 424 | 425 | # Mac bundle stuff 426 | *.dmg 427 | *.app 428 | 429 | # content below from: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore 430 | # General 431 | .DS_Store 432 | .AppleDouble 433 | .LSOverride 434 | 435 | # Icon must end with two \r 436 | Icon 437 | 438 | 439 | # Thumbnails 440 | ._* 441 | 442 | # Files that might appear in the root of a volume 443 | .DocumentRevisions-V100 444 | .fseventsd 445 | .Spotlight-V100 446 | .TemporaryItems 447 | .Trashes 448 | .VolumeIcon.icns 449 | .com.apple.timemachine.donotpresent 450 | 451 | # Directories potentially created on remote AFP share 452 | .AppleDB 453 | .AppleDesktop 454 | Network Trash Folder 455 | Temporary Items 456 | .apdisk 457 | 458 | # content below from: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore 459 | # Windows thumbnail cache files 460 | Thumbs.db 461 | ehthumbs.db 462 | ehthumbs_vista.db 463 | 464 | # Dump file 465 | *.stackdump 466 | 467 | # Folder config file 468 | [Dd]esktop.ini 469 | 470 | # Recycle Bin used on file shares 471 | $RECYCLE.BIN/ 472 | 473 | # Windows Installer files 474 | *.cab 475 | *.msi 476 | *.msix 477 | *.msm 478 | *.msp 479 | 480 | # Windows shortcuts 481 | *.lnk 482 | 483 | # Vim temporary swap files 484 | *.swp 485 | 486 | # fsdocs 487 | tmp/ 488 | .fsdocs/ 489 | output/ 490 | 491 | # VSCode 492 | .vscode/ -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html) from version [0.1.0] moving forward. 7 | 8 | ## [0.1.1] - 2025-06-03 9 | ### Fixed 10 | - Support `major` roll forward dotnet versions of runtime for Fable.Daemon, pr ([#46](https://github.com/fable-compiler/vite-plugin-fable/pull/46)), targets issue ([#44](https://github.com/fable-compiler/vite-plugin-fable/issues/44)) 11 | 12 | ## [0.1.0] - 2025-05-22 13 | ### Changed 14 | - bumping version and package release for changelog sync 15 | 16 | ## [0.0.37] - 2025-05-10 17 | ### Changed 18 | - Fable.Compiler updated to 5.0.0-alpha.13 ([#38](https://github.com/fable-compiler/vite-plugin-fable/pull/38)) 19 | - Added caret range to fable-library-js ([#38](https://github.com/fable-compiler/vite-plugin-fable/pull/38)) 20 | - Updated fable-library-js to 2.0.0-beta.3 ([#38](https://github.com/fable-compiler/vite-plugin-fable/pull/38)) 21 | 22 | ## [0.0.36] - 2025-05-07 23 | ### Changed 24 | - Updated fable-library-js to ^2.0.0-beta.3 ([#38](https://github.com/fable-compiler/vite-plugin-fable/pull/38)) 25 | 26 | ## [0.0.35] - 2025-04-30 27 | ### Added 28 | - Added README for Fable Daemon ([#34](https://github.com/fable-compiler/vite-plugin-fable/pull/34)) 29 | 30 | ## [0.0.34] - 2025-04-20 31 | ### Changed 32 | - Upgrade to latest Fable.Compiler and Fable.AST ([#13](https://github.com/fable-compiler/vite-plugin-fable/pull/13)) 33 | 34 | ## [0.0.33] - 2025-04-10 35 | ### Changed 36 | - Update node dependencies and bump version 37 | 38 | ## [0.0.32] - 2025-03-30 39 | ### Fixed 40 | - Fix Thoth.Json usage ([#23](https://github.com/fable-compiler/vite-plugin-fable/pull/23)) 41 | 42 | ## [0.0.31] - 2025-03-15 43 | ### Added 44 | - Error overlay for development ([#8](https://github.com/fable-compiler/vite-plugin-fable/pull/8)) 45 | 46 | ## [0.0.30] - 2025-03-01 47 | ### Added 48 | - Vite 6 support ([#11](https://github.com/fable-compiler/vite-plugin-fable/pull/11)) 49 | 50 | ## [0.0.29] - 2025-02-20 51 | ### Changed 52 | - Improved diagnostics ([#3](https://github.com/fable-compiler/vite-plugin-fable/pull/3)) 53 | 54 | ## [0.0.28] - 2025-02-10 55 | ### Added 56 | - Debug viewer ([#5](https://github.com/fable-compiler/vite-plugin-fable/pull/5)) 57 | 58 | ## [0.0.27] - 2025-01-30 59 | ### Changed 60 | - Combine file changes for faster rebuilds ([#6](https://github.com/fable-compiler/vite-plugin-fable/pull/6)) 61 | 62 | ## [0.0.26] - 2025-01-15 63 | ### Added 64 | - Project options cache ([#2](https://github.com/fable-compiler/vite-plugin-fable/pull/2)) 65 | 66 | ## [0.0.25] - 2025-01-05 67 | ### Added 68 | - Support for arm64 architecture in postinstall ([#1](https://github.com/fable-compiler/vite-plugin-fable/pull/1)) 69 | 70 | ## [0.0.24] - 2024-03-02 71 | ### Changed 72 | - Improved endpoint call control via shared pending changes subscription. 73 | - Various internal improvements and bug fixes. 74 | 75 | ## [0.0.22] - 2024-02-28 76 | ### Changed 77 | - Update Fable.Compiler to 4.0.0-alpha-008. 78 | - Update TypeScript and include debug folder. 79 | 80 | ## [0.0.20] - 2024-02-26 81 | ### Changed 82 | - Handle F# changes via handleHotUpdate callback. 83 | - Improved file change tracking and project cache key logic. 84 | 85 | ## [0.0.18] - 2024-02-25 86 | ### Changed 87 | - Only send sourceFiles list of FSharpProjectOptions to plugin. 88 | - Additional logging and reuse of CrackerOptions. 89 | 90 | ## [0.0.16] - 2024-02-24 91 | ### Added 92 | - Debug documentation and error overlay prototype. 93 | - Initial debug page setup. 94 | 95 | ## [0.0.7] - 2024-02-13 96 | ### Added 97 | - Diagnostics support ([#3](https://github.com/fable-compiler/vite-plugin-fable/pull/3)). 98 | - Use @fable-org/fable-library-js. 99 | 100 | ## [0.0.3] - 2024-02-05 101 | ### Added 102 | - Thoth.Json support. 103 | - Initial cache key setup for project configuration. 104 | - Initial caching for design time build. 105 | 106 | ## [0.0.1] - 2023-10-28 107 | ### Added 108 | - Initial implementation of Vite plugin for Fable. 109 | - Basic F# file compilation and integration with Vite build. 110 | - Early support for project file watching and hot reload. 111 | - Initial project setup, configuration, and documentation. -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | true 4 | en 5 | true 6 | $(NoWarn);FS0075 7 | $(OtherFlags) --parallelreferenceresolution --test:GraphBasedChecking --test:ParallelOptimization --test:ParallelIlxGen --strict-indentation+ 8 | false 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | https://github.com/fable-compiler/vite-plugin-fable 21 | http://fable.io/vite-plugin-fable/ 22 | https://github.com/fable-compiler/vite-plugin-fable/blob/main/LICENSE 23 | Florian Verdonck 24 | 25 | -------------------------------------------------------------------------------- /Directory.Packages.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | true 4 | true 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /Fable.Daemon.Tests/DebugTests.fs: -------------------------------------------------------------------------------- 1 | module Fable.Daemon.Tests 2 | 3 | open System 4 | open System.IO 5 | open Microsoft.Extensions.Logging.Abstractions 6 | open NUnit.Framework 7 | open Nerdbank.Streams 8 | open StreamJsonRpc 9 | open Fable.Daemon 10 | 11 | type Path with 12 | static member CombineNormalize ([] parts : string array) = Path.Combine parts |> Path.GetFullPath 13 | 14 | let fableLibrary = 15 | Path.CombineNormalize (__SOURCE_DIRECTORY__, "../node_modules/@fable-org/fable-library-js") 16 | 17 | let sampleApp = 18 | { 19 | Project = Path.CombineNormalize (__SOURCE_DIRECTORY__, "../sample-project/App.fsproj") 20 | FableLibrary = fableLibrary 21 | Configuration = "Release" 22 | Exclude = Array.empty 23 | NoReflection = false 24 | } 25 | 26 | let telplin = 27 | { 28 | Project = Path.CombineNormalize (__SOURCE_DIRECTORY__, "../../telplin/tool/client/OnlineTool.fsproj") 29 | FableLibrary = fableLibrary 30 | Configuration = "Debug" 31 | Exclude = Array.empty 32 | NoReflection = false 33 | } 34 | 35 | let fantomasTools = 36 | { 37 | Project = 38 | Path.CombineNormalize (__SOURCE_DIRECTORY__, "../../fantomas-tools/src/client/fsharp/FantomasTools.fsproj") 39 | FableLibrary = fableLibrary 40 | Configuration = "Debug" 41 | Exclude = Array.empty 42 | NoReflection = false 43 | } 44 | 45 | // let ronnies = 46 | // { 47 | // Project = @"C:\Users\nojaf\Projects\ronnies.be\app\App.fsproj" 48 | // FableLibrary = fableLibrary 49 | // Configuration = "Debug" 50 | // Exclude = [| "Nojaf.Fable.React.Plugin" |] 51 | // NoReflection = true 52 | // } 53 | 54 | [] 55 | let DebugTest () = 56 | task { 57 | let config = sampleApp 58 | Directory.SetCurrentDirectory (FileInfo(config.Project).DirectoryName) 59 | 60 | let struct (serverStream, clientStream) = FullDuplexStream.CreatePair () 61 | 62 | let daemon = 63 | new Program.FableServer (serverStream, serverStream, NullLogger.Instance) 64 | 65 | let client = new JsonRpc (clientStream, clientStream) 66 | client.StartListening () 67 | 68 | let! typecheckResponse = daemon.ProjectChanged config 69 | ignore typecheckResponse 70 | 71 | let! compileFiles = 72 | daemon.CompileFiles 73 | { 74 | FileNames = 75 | [| 76 | Path.CombineNormalize (FileInfo(sampleApp.Project).Directory.FullName, "Math.fs") 77 | |] 78 | } 79 | 80 | printfn "response: %A" compileFiles 81 | client.Dispose () 82 | (daemon :> IDisposable).Dispose () 83 | 84 | Assert.Pass () 85 | } 86 | -------------------------------------------------------------------------------- /Fable.Daemon.Tests/Fable.Daemon.Tests.fsproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | false 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Fable.Daemon.Tests/Program.fs: -------------------------------------------------------------------------------- 1 | module Program = 2 | 3 | [] 4 | let main _ = 0 5 | -------------------------------------------------------------------------------- /Fable.Daemon/Caching.fs: -------------------------------------------------------------------------------- 1 | module Fable.Daemon.Caching 2 | 3 | open System 4 | open System.IO 5 | open System.Reflection 6 | open System.Runtime.InteropServices 7 | open Microsoft.Extensions.Logging 8 | open Thoth.Json.Core 9 | open Thoth.Json.System.Text.Json 10 | open ProtoBuf 11 | open Fable.Compiler.ProjectCracker 12 | 13 | [] 14 | let DesignTimeBuildExtension = ".vite-plugin-design-time" 15 | 16 | [] 17 | let FableModulesExtension = ".vite-plugin-fable-modules" 18 | 19 | let fableCompilerVersion = 20 | let assembly = typeof.Assembly 21 | 22 | let attribute = 23 | assembly.GetCustomAttribute () 24 | 25 | attribute.InformationalVersion 26 | 27 | /// Calculates the SHA256 hash of the given file. 28 | type FileInfo with 29 | member this.Hash : string = 30 | use sha256 = System.Security.Cryptography.SHA256.Create () 31 | use stream = File.OpenRead this.FullName 32 | let hash = sha256.ComputeHash stream 33 | BitConverter.ToString(hash).Replace ("-", "") 34 | 35 | [] 36 | type InvalidCacheReason = 37 | | FileDoesNotExist of cacheFile : FileInfo 38 | | CouldNotDeserialize of error : string 39 | | MainFsprojChanged 40 | | DefinesMismatch of cachedDefines : Set * currentDefines : Set 41 | | DependentFileCountDoesNotMatch of cachedCount : int * currentCount : int 42 | | DependentFileHashMismatch of file : FileInfo 43 | | FableCompilerVersionMismatch of cachedVersion : string * currentVersion : string 44 | 45 | /// Contains all the info that determines the cache design time build value. 46 | /// This is not the cached information! 47 | type CacheKey = 48 | { 49 | /// Input fsproj project. 50 | MainFsproj : FileInfo 51 | /// This is the file that contains the cached information. 52 | /// Initially it doesn't exist and can only be checked in subsequent runs. 53 | CacheFile : FileInfo 54 | /// All the files that can influence the MSBuild evaluation. 55 | /// This typically is the 56 | DependentFiles : FileInfo list 57 | /// Contains both the user defined configurations (via Vite plugin options) 58 | Defines : Set 59 | /// Configuration 60 | Configuration : string 61 | /// AssemblyInformationalVersion of Fable.Compiler 62 | FableCompilerVersion : string 63 | } 64 | 65 | member x.FableModulesCacheFile = 66 | Path.ChangeExtension (x.CacheFile.FullName, FableModulesExtension) |> FileInfo 67 | 68 | [] 69 | [] 70 | type KeyValuePairProto = 71 | { 72 | [] 73 | Key : string 74 | [] 75 | Value : string 76 | } 77 | 78 | [] 79 | [] 80 | type DesignTimeBuildCache = 81 | { 82 | [] 83 | MainFsproj : string 84 | [] 85 | DependentFiles : KeyValuePairProto array 86 | [] 87 | Defines : string array 88 | [] 89 | ProjectOptions : string array 90 | [] 91 | ProjectReferences : string array 92 | [] 93 | OutputType : string option 94 | [] 95 | TargetFramework : string option 96 | [] 97 | FableCompilerVersion : string 98 | } 99 | 100 | let private isWindows = RuntimeInformation.IsOSPlatform OSPlatform.Windows 101 | 102 | /// Save the compiler arguments results from the design time build to the intermediate folder. 103 | let writeDesignTimeBuild (x : CacheKey) (response : ProjectOptionsResponse) = 104 | use fs = File.Create x.CacheFile.FullName 105 | 106 | let dependentFiles = 107 | [| 108 | for df in x.DependentFiles do 109 | yield { Key = df.FullName ; Value = df.Hash } 110 | |] 111 | 112 | let data = 113 | { 114 | MainFsproj = x.MainFsproj.Hash 115 | DependentFiles = dependentFiles 116 | Defines = Set.toArray x.Defines 117 | ProjectOptions = response.ProjectOptions 118 | ProjectReferences = response.ProjectReferences 119 | OutputType = response.OutputType 120 | TargetFramework = response.TargetFramework 121 | FableCompilerVersion = x.FableCompilerVersion 122 | } 123 | 124 | Serializer.Serialize (fs, data) 125 | 126 | let private emptyArrayIfNull a = if isNull a then Array.empty else a 127 | 128 | /// Verify is the cached key for the project exists and is still valid. 129 | let canReuseDesignTimeBuildCache (cacheKey : CacheKey) : Result = 130 | if not cacheKey.CacheFile.Exists then 131 | Error (InvalidCacheReason.FileDoesNotExist cacheKey.CacheFile) 132 | else 133 | 134 | try 135 | use fs = File.OpenRead cacheKey.CacheFile.FullName 136 | let cacheContent = Serializer.Deserialize fs 137 | let cachedDefines = Set.ofArray cacheContent.Defines 138 | 139 | if fableCompilerVersion <> cacheContent.FableCompilerVersion then 140 | Error ( 141 | InvalidCacheReason.FableCompilerVersionMismatch ( 142 | cacheContent.FableCompilerVersion, 143 | fableCompilerVersion 144 | ) 145 | ) 146 | elif cacheKey.MainFsproj.Hash <> cacheContent.MainFsproj then 147 | Error InvalidCacheReason.MainFsprojChanged 148 | elif cacheKey.Defines <> cachedDefines then 149 | Error (InvalidCacheReason.DefinesMismatch (cachedDefines, cacheKey.Defines)) 150 | elif cacheKey.DependentFiles.Length <> cacheContent.DependentFiles.Length then 151 | Error ( 152 | InvalidCacheReason.DependentFileCountDoesNotMatch ( 153 | cacheContent.DependentFiles.Length, 154 | cacheKey.DependentFiles.Length 155 | ) 156 | ) 157 | else 158 | 159 | // Verify if each dependent files was found in the cached data and if the hashes still match. 160 | let mismatchedFile = 161 | (cacheKey.DependentFiles, cacheContent.DependentFiles) 162 | ||> Seq.zip 163 | |> Seq.tryFind (fun (df, cachedDF) -> df.FullName <> cachedDF.Key || df.Hash <> cachedDF.Value) 164 | |> Option.map fst 165 | 166 | match mismatchedFile with 167 | | Some mmf -> Error (InvalidCacheReason.DependentFileHashMismatch mmf) 168 | | None -> 169 | 170 | let projectOptionsResponse : ProjectOptionsResponse = 171 | { 172 | ProjectOptions = emptyArrayIfNull cacheContent.ProjectOptions 173 | ProjectReferences = emptyArrayIfNull cacheContent.ProjectReferences 174 | OutputType = cacheContent.OutputType 175 | TargetFramework = cacheContent.TargetFramework 176 | } 177 | 178 | Ok projectOptionsResponse 179 | with ex -> 180 | Error (InvalidCacheReason.CouldNotDeserialize ex.Message) 181 | 182 | let private cacheKeyDecoder (options : CrackerOptions) (fsproj : FileInfo) : Decoder = 183 | Decode.object (fun get -> 184 | let paths = 185 | let value = get.Required.At [ "Properties" ; "MSBuildAllProjects" ] Decode.string 186 | 187 | value.Split (';', StringSplitOptions.RemoveEmptyEntries) 188 | |> Array.choose (fun path -> 189 | let fi = FileInfo path 190 | 191 | if not fi.Exists then None else Some fi 192 | ) 193 | 194 | // if `UseArtifactsOutput=true` then the IntermediateOutputPath the path is absolute "C:\Users\nojaf\Projects\telplin\artifacts\obj\OnlineTool\debug" 195 | // else it is something like "obj\\Release/net7.0/", on Linux slashes can be mixed 🙃 196 | let intermediateOutputPath = 197 | let v = get.Required.At [ "Properties" ; "IntermediateOutputPath" ] Decode.string 198 | let v = if isWindows then v else v.Replace ('\\', '/') 199 | let v = v.TrimEnd [| '\\' ; '/' |] 200 | Path.Combine (fsproj.DirectoryName, v) |> Path.GetFullPath 201 | 202 | // Full path of the folder that contains the `g.props` file. 203 | let msbuildProjectExtensionsPath = 204 | get.Required.At [ "Properties" ; "MSBuildProjectExtensionsPath" ] Decode.string 205 | 206 | let nugetGProps = 207 | let gPropFile = 208 | Path.Combine (msbuildProjectExtensionsPath, $"%s{fsproj.Name}.nuget.g.props") 209 | |> FileInfo 210 | 211 | if not gPropFile.Exists then [] else [ gPropFile ] 212 | 213 | let cacheFile = 214 | FileInfo (Path.Combine (intermediateOutputPath, $"{fsproj.Name}%s{DesignTimeBuildExtension}")) 215 | 216 | let dependentFiles = 217 | [ yield fsproj ; yield! paths ; yield! nugetGProps ] 218 | |> List.distinctBy (fun fi -> fi.FullName) 219 | 220 | { 221 | MainFsproj = fsproj 222 | CacheFile = cacheFile 223 | DependentFiles = dependentFiles 224 | Defines = Set.ofList options.FableOptions.Define 225 | Configuration = options.Configuration 226 | FableCompilerVersion = fableCompilerVersion 227 | } 228 | ) 229 | 230 | /// Generate the caching key information for the design time build of the incoming fsproj file. 231 | let mkProjectCacheKey 232 | (logger : ILogger) 233 | (options : CrackerOptions) 234 | (fsproj : FileInfo) 235 | : Async> 236 | = 237 | async { 238 | if not fsproj.Exists then 239 | raise (ArgumentException ($"%s{fsproj.FullName} does not exists", nameof fsproj)) 240 | 241 | if String.IsNullOrWhiteSpace options.Configuration then 242 | raise ( 243 | ArgumentException ("options.Configuration cannot be null or whitespace", nameof options.Configuration) 244 | ) 245 | 246 | let! json = 247 | MSBuild.dotnet_msbuild 248 | logger 249 | fsproj 250 | $"/p:Configuration=%s{options.Configuration} --getProperty:MSBuildAllProjects --getProperty:IntermediateOutputPath --getProperty:MSBuildProjectExtensionsPath" 251 | 252 | return Decode.fromString (cacheKeyDecoder options fsproj) json 253 | } 254 | 255 | [] 256 | [] 257 | type FableModulesProto = 258 | { 259 | [] 260 | Files : KeyValuePairProto array 261 | } 262 | 263 | /// Try and load the previous compiled fable-modules files. 264 | /// These should not change if the cache remained stable. 265 | let loadFableModulesFromCache (cacheKey : CacheKey) : Map = 266 | if not cacheKey.FableModulesCacheFile.Exists then 267 | Map.empty 268 | else 269 | 270 | try 271 | use fs = File.OpenRead cacheKey.FableModulesCacheFile.FullName 272 | let { Files = files } = Serializer.Deserialize fs 273 | 274 | files 275 | |> emptyArrayIfNull 276 | |> Array.map (fun kv -> kv.Key, kv.Value) 277 | |> Map.ofArray 278 | with ex -> 279 | Map.empty 280 | 281 | let writeFableModulesFromCache (cacheKey : CacheKey) (fableModuleFiles : Map) = 282 | try 283 | let proto : FableModulesProto = 284 | let files = 285 | fableModuleFiles.Keys 286 | |> Seq.map (fun key -> 287 | { 288 | Key = key 289 | Value = fableModuleFiles.[key] 290 | } 291 | ) 292 | |> Seq.toArray 293 | 294 | { Files = files } 295 | 296 | use fs = File.Create cacheKey.FableModulesCacheFile.FullName 297 | Serializer.Serialize (fs, proto) 298 | finally 299 | () 300 | -------------------------------------------------------------------------------- /Fable.Daemon/CoolCatCracking.fs: -------------------------------------------------------------------------------- 1 | namespace Fable.Daemon 2 | 3 | open System 4 | open System.IO 5 | open System.Collections.Concurrent 6 | open Microsoft.Extensions.Logging 7 | open Thoth.Json.Core 8 | open Thoth.Json.System.Text.Json 9 | open Fable 10 | open Fable.Compiler.ProjectCracker 11 | 12 | module CoolCatCracking = 13 | 14 | let fsharpFiles = set [| ".fs" ; ".fsi" ; ".fsx" |] 15 | 16 | let isFSharpFile (file : string) = 17 | Set.exists (fun (ext : string) -> file.EndsWith (ext, StringComparison.Ordinal)) fsharpFiles 18 | 19 | /// Transform F# files into full paths 20 | let private mkOptions (projectFile : FileInfo) (compilerArgs : string array) : string array = 21 | compilerArgs 22 | |> Array.map (fun (line : string) -> 23 | if not (isFSharpFile line) then 24 | line 25 | else 26 | Path.Combine (projectFile.DirectoryName, line) |> Path.GetFullPath 27 | ) 28 | 29 | let private identityDecoder = 30 | Decode.object (fun get -> get.Required.Field "Identity" Decode.string) 31 | 32 | /// Perform a design time build using the `dotnet msbuild` cli invocation. 33 | let mkOptionsFromDesignTimeBuildAux 34 | (logger : ILogger) 35 | (fsproj : FileInfo) 36 | (options : CrackerOptions) 37 | : Async 38 | = 39 | async { 40 | let! targetFrameworkJson = 41 | let configuration = 42 | if String.IsNullOrWhiteSpace options.Configuration then 43 | "" 44 | else 45 | $"/p:Configuration=%s{options.Configuration}" 46 | 47 | MSBuild.dotnet_msbuild 48 | logger 49 | fsproj 50 | $"{configuration} --getProperty:TargetFrameworks --getProperty:TargetFramework" 51 | 52 | // To perform a design time build we need to target an exact single TargetFramework 53 | // There is a slight chance that the fsproj uses net8.0 54 | // We need to take this into account. 55 | let targetFramework = 56 | let decoder = 57 | Decode.object (fun get -> 58 | get.Required.At [ "Properties" ; "TargetFramework" ] Decode.string, 59 | get.Required.At [ "Properties" ; "TargetFrameworks" ] Decode.string 60 | ) 61 | 62 | match Decode.fromString decoder targetFrameworkJson with 63 | | Error e -> failwithf $"Could not decode target framework json, %A{e}" 64 | | Ok (tf, tfs) -> 65 | 66 | if not (String.IsNullOrWhiteSpace tf) then 67 | tf 68 | else 69 | tfs.Split ';' |> Array.head 70 | 71 | logger.LogDebug ("Perform design time build for {targetFramework}", targetFramework) 72 | 73 | // TRACE is typically present for fsproj projects 74 | let defines = options.FableOptions.Define 75 | 76 | // When CoreCompile does not need a rebuild, MSBuild will skip that target and thus will not populate the FscCommandLineArgs items. 77 | // To overcome this we want to force a design time build, using the NonExistentFile property helps prevent a cache hit. 78 | let nonExistentFile = Path.Combine ("__NonExistentSubDir__", "__NonExistentFile__") 79 | 80 | let properties = 81 | [ 82 | "/p:VitePlugin=True" 83 | if not (String.IsNullOrWhiteSpace options.Configuration) then 84 | $"/p:Configuration=%s{options.Configuration}" 85 | $"/p:TargetFramework=%s{targetFramework}" 86 | "/p:DesignTimeBuild=True" 87 | "/p:SkipCompilerExecution=True" 88 | // This will populate FscCommandLineArgs 89 | "/p:ProvideCommandLineArgs=True" 90 | // See https://github.com/NuGet/Home/issues/13046 91 | "/p:RestoreUseStaticGraphEvaluation=False" 92 | // Avoid restoring with an existing lock file 93 | "/p:RestoreLockedMode=false" 94 | "/p:RestorePackagesWithLockFile=false" 95 | // We trick NuGet into believing there is no lock file create, if it does exist it will try and create it. 96 | " /p:NuGetLockFilePath=VitePlugin.lock" 97 | // Avoid skipping the CoreCompile target via this property. 98 | $"/p:NonExistentFile=\"%s{nonExistentFile}\"" 99 | ] 100 | |> List.filter (String.IsNullOrWhiteSpace >> not) 101 | |> String.concat " " 102 | 103 | // We do not specify the Restore target itself, the `/restore` flag will take care of this. 104 | // Imagine with me for a moment how MSBuild works for a given project: 105 | // 106 | // it opens the project file 107 | // it reads and loads the MSBuild SDKs specified in the project file 108 | // it follows any Imports in those props/targets 109 | // it then executes the targets involved 110 | // this is why the /restore flag exists - this tells MSBuild-the-engine to do an entirely separate call to /t:Restore before whatever you specified, 111 | // so that the targets you specified run against a fully-correct local environment with all the props/targets files 112 | let targets = 113 | "ResolveAssemblyReferencesDesignTime,ResolveProjectReferencesDesignTime,ResolvePackageDependenciesDesignTime,FindReferenceAssembliesForReferences,_GenerateCompileDependencyCache,_ComputeNonExistentFileProperty,BeforeBuild,BeforeCompile,CoreCompile" 114 | 115 | // NU1608: Detected package version outside of dependency constraint, see https://learn.microsoft.com/en-us/nuget/reference/errors-and-warnings/nu1608 116 | let arguments = 117 | $"/restore /t:%s{targets} %s{properties} -warnAsMessage:NU1608 -warnAsMessage:NU1605 --getItem:FscCommandLineArgs --getItem:ProjectReference --getProperty:OutputType" 118 | 119 | let! json = MSBuild.dotnet_msbuild_with_defines logger fsproj arguments defines 120 | 121 | let decoder = 122 | Decode.object (fun get -> 123 | let options = 124 | get.Required.At [ "Items" ; "FscCommandLineArgs" ] (Decode.array identityDecoder) 125 | 126 | let projectReferences = 127 | get.Required.At [ "Items" ; "ProjectReference" ] (Decode.array identityDecoder) 128 | 129 | let outputType = get.Required.At [ "Properties" ; "OutputType" ] Decode.string 130 | options, projectReferences, outputType 131 | ) 132 | 133 | match Decode.fromString decoder json with 134 | | Error e -> return failwithf $"Could not decode the design time build json, %A{e}" 135 | | Ok (options, projectReferences, outputType) -> 136 | 137 | if Array.isEmpty options then 138 | logger.LogCritical ( 139 | "Design time build for {fsproj} failed. CoreCompile was most likely skipped.", 140 | fsproj.FullName 141 | ) 142 | 143 | return 144 | failwithf 145 | $"Design time build for %s{fsproj.FullName} failed. CoreCompile was most likely skipped. `dotnet clean` might help here.\ndotnet msbuild %s{fsproj.FullName} %s{arguments}" 146 | else 147 | 148 | logger.LogDebug ("Design time build for {fsproj} completed.", fsproj) 149 | let options = mkOptions fsproj options 150 | 151 | let projectReferences = 152 | projectReferences 153 | |> Seq.map (fun relativePath -> Path.Combine (fsproj.DirectoryName, relativePath) |> Path.GetFullPath) 154 | |> Seq.toArray 155 | 156 | return 157 | { 158 | ProjectOptions = options 159 | ProjectReferences = projectReferences 160 | OutputType = Some outputType 161 | TargetFramework = Some targetFramework 162 | } 163 | } 164 | 165 | /// Crack the fsproj using the `dotnet msbuild --getProperty --getItem` command 166 | /// See https://devblogs.microsoft.com/dotnet/announcing-dotnet-8-rc2/#msbuild-simple-cli-based-project-evaluation 167 | type CoolCatResolver(logger : ILogger) = 168 | let cached = ConcurrentDictionary () 169 | 170 | /// Under the same design time conditions and same Fable.Compiler, the used Fable libraries don't change. 171 | member x.TryGetCachedFableModuleFiles (fsproj : FullPath) : Map = 172 | if not (cached.ContainsKey fsproj) then 173 | logger.LogWarning ("{fsproj} does not have a cache entry in CoolCatResolver", fsproj) 174 | Map.empty 175 | else 176 | Caching.loadFableModulesFromCache cached.[fsproj] 177 | 178 | /// Try and write the fable_module compilation results to the cache. 179 | member x.WriteCachedFableModuleFiles (fsproj : FullPath) (fableModuleFiles : Map) = 180 | if not (cached.ContainsKey fsproj) then 181 | logger.LogWarning ("{fsproj} does not have a cache entry in CoolCatResolver", fsproj) 182 | else 183 | 184 | Caching.writeFableModulesFromCache cached.[fsproj] fableModuleFiles 185 | 186 | /// Get project files to watch inside the plugin 187 | /// These are the fsproj and potential MSBuild import files 188 | member x.MSBuildProjectFiles (fsproj : FullPath) : FileInfo list = 189 | if not (cached.ContainsKey fsproj) then 190 | logger.LogWarning ("{fsproj} does not have a cache entry in CoolCatResolver", fsproj) 191 | List.empty 192 | else 193 | cached.[fsproj].DependentFiles 194 | 195 | interface ProjectCrackerResolver with 196 | member x.GetProjectOptionsFromProjectFile (isMain, options, projectFile) = 197 | async { 198 | logger.LogDebug ("ProjectCrackerResolver.GetProjectOptionsFromProjectFile {projectFile}", projectFile) 199 | let fsproj = FileInfo projectFile 200 | 201 | if not fsproj.Exists then 202 | invalidArg (nameof fsproj) $"\"%s{fsproj.FullName}\" does not exist." 203 | 204 | let! currentCacheKey = 205 | async { 206 | if cached.ContainsKey fsproj.FullName then 207 | return cached.[fsproj.FullName] 208 | else 209 | match! Caching.mkProjectCacheKey logger options fsproj with 210 | | Error error -> 211 | logger.LogError ( 212 | "Could not construct cache key for {projectFile} {error}", 213 | projectFile, 214 | error 215 | ) 216 | 217 | return failwithf $"Could not construct cache key for %s{projectFile}, %A{error}" 218 | | Ok cacheKey -> return cacheKey 219 | } 220 | 221 | cached.AddOrUpdate (fsproj.FullName, (fun _ -> currentCacheKey), (fun _ _ -> currentCacheKey)) 222 | |> ignore 223 | 224 | match Caching.canReuseDesignTimeBuildCache currentCacheKey with 225 | | Ok projectOptionsResponse -> 226 | logger.LogInformation ("Design time build cache can be reused for {projectFile}", projectFile) 227 | // The sweet spot, nothing changed and we can skip the design time build 228 | return projectOptionsResponse 229 | | Error reason -> 230 | logger.LogDebug ( 231 | "Cache file could not be reused for {projectFile} because {reason}", 232 | projectFile, 233 | reason 234 | ) 235 | 236 | // Delete the current cache file if it is no longer valid. 237 | match reason with 238 | | Caching.InvalidCacheReason.CouldNotDeserialize _ 239 | | Caching.InvalidCacheReason.FableCompilerVersionMismatch _ 240 | | Caching.InvalidCacheReason.MainFsprojChanged 241 | | Caching.InvalidCacheReason.DefinesMismatch _ 242 | | Caching.InvalidCacheReason.DependentFileCountDoesNotMatch _ 243 | | Caching.InvalidCacheReason.DependentFileHashMismatch _ -> 244 | try 245 | if currentCacheKey.CacheFile.Exists then 246 | File.Delete currentCacheKey.CacheFile.FullName 247 | 248 | if currentCacheKey.FableModulesCacheFile.Exists then 249 | File.Delete currentCacheKey.FableModulesCacheFile.FullName 250 | finally 251 | () 252 | | Caching.InvalidCacheReason.FileDoesNotExist _ -> () 253 | 254 | // Perform design time build and cache result 255 | logger.LogDebug ("About to perform design time build for {projectFile}", projectFile) 256 | let! result = CoolCatCracking.mkOptionsFromDesignTimeBuildAux logger fsproj options 257 | Caching.writeDesignTimeBuild currentCacheKey result 258 | 259 | return result 260 | } 261 | |> Async.RunSynchronously 262 | -------------------------------------------------------------------------------- /Fable.Daemon/Debug.fs: -------------------------------------------------------------------------------- 1 | module Fable.Daemon.Debug 2 | 3 | open System 4 | open System.Collections.Generic 5 | open System.Text 6 | open System.Collections.Concurrent 7 | open System.IO 8 | open System.Net 9 | open System.Threading 10 | open Suave 11 | open Suave.Filters 12 | open Suave.Operators 13 | open Suave.Successful 14 | open Suave.Logging 15 | open Suave.Sockets 16 | open Suave.Sockets.Control 17 | open Suave.WebSocket 18 | open Microsoft.Extensions.Logging 19 | 20 | let defaultPort = 9014us 21 | 22 | /// We can't log anything to the stdout! 23 | let zeroSuaveLogger : Logger = 24 | { new Logger with 25 | member x.log level _ = () 26 | member x.logWithAck _ _ = async.Zero () 27 | member x.name = [| "vite-plugin-fable" |] 28 | } 29 | 30 | let homeFolder = Path.Combine (__SOURCE_DIRECTORY__, "debug") 31 | 32 | type LogEntry = 33 | { 34 | Level : string 35 | Exception : exn 36 | Message : string 37 | TimeStamp : DateTime 38 | } 39 | 40 | module HTML = 41 | open Fable.React 42 | 43 | let mapLogEntriesToListItems (logEntries : LogEntry seq) = 44 | logEntries 45 | |> Seq.map (fun entry -> 46 | li [] [ 47 | strong [] [ str entry.Level ] 48 | time [] [ str (entry.TimeStamp.ToString "HH:mm:ss.fff") ] 49 | pre [] [ str entry.Message ] 50 | ] 51 | ) 52 | |> fragment [] 53 | |> Fable.ReactServer.renderToString 54 | 55 | /// Dictionary of client and how many messages they received 56 | let connectedClients = ConcurrentDictionary () 57 | 58 | type InMemoryLogger() = 59 | let entries = Queue () 60 | 61 | let broadCastNewMessages () = 62 | for KeyValue (client, currentCount) in connectedClients do 63 | let messages = 64 | entries 65 | |> Seq.skip currentCount 66 | |> HTML.mapLogEntriesToListItems 67 | |> Encoding.UTF8.GetBytes 68 | |> ByteSegment 69 | 70 | client.send Text messages true // 71 | |> Async.Ignore 72 | |> Async.RunSynchronously 73 | 74 | connectedClients.[client] <- entries.Count 75 | 76 | member val All : LogEntry seq = entries 77 | member x.Count : int = entries.Count 78 | 79 | interface ILogger with 80 | member x.Log<'TState> 81 | ( 82 | logLevel : LogLevel, 83 | _eventId : EventId, 84 | state : 'TState, 85 | ex : exn, 86 | formatter : System.Func<'TState, exn, string> 87 | ) 88 | : unit 89 | = 90 | entries.Enqueue 91 | { 92 | Level = string logLevel 93 | Exception = ex 94 | Message = formatter.Invoke (state, ex) 95 | TimeStamp = DateTime.Now 96 | } 97 | 98 | broadCastNewMessages () 99 | 100 | member x.BeginScope<'TState> (_state : 'TState) : IDisposable = null 101 | member x.IsEnabled (_logLevel : LogLevel) : bool = true 102 | 103 | let ws (logger : InMemoryLogger) (webSocket : WebSocket) (context : HttpContext) = 104 | context.runtime.logger.info (Message.eventX $"New websocket connection") 105 | connectedClients.TryAdd (webSocket, logger.Count) |> ignore 106 | 107 | socket { 108 | let mutable loop = true 109 | 110 | while loop do 111 | let! msg = webSocket.read () 112 | 113 | match msg with 114 | | Close, _, _ -> 115 | connectedClients.TryRemove webSocket |> ignore 116 | let emptyResponse = [||] |> ByteSegment 117 | do! webSocket.send Close emptyResponse true 118 | loop <- false 119 | 120 | | _ -> () 121 | } 122 | 123 | let webApp (logger : InMemoryLogger) : WebPart = 124 | let allLogs ctx = 125 | let html = logger.All |> HTML.mapLogEntriesToListItems 126 | (OK html >=> Writers.setMimeType "text/html") ctx 127 | 128 | choose [ 129 | path "/ws" >=> handShake (ws logger) 130 | GET >=> path "/" >=> Files.browseFile homeFolder "index.html" 131 | GET >=> path "/all" >=> allLogs 132 | GET >=> Files.browseHome 133 | RequestErrors.NOT_FOUND "Page not found." 134 | ] 135 | 136 | let startWebserver (logger : InMemoryLogger) (cancellationToken : CancellationToken) : Async = 137 | let conf = 138 | { defaultConfig with 139 | cancellationToken = cancellationToken 140 | homeFolder = Some homeFolder 141 | logger = zeroSuaveLogger 142 | bindings = [ HttpBinding.create HTTP IPAddress.Loopback defaultPort ] 143 | } 144 | 145 | (logger :> ILogger).LogDebug "Starting Suave dev server" 146 | let _listening, server = startWebServerAsync conf (webApp logger) 147 | server 148 | -------------------------------------------------------------------------------- /Fable.Daemon/Debug.fsi: -------------------------------------------------------------------------------- 1 | module Fable.Daemon.Debug 2 | 3 | open System.Threading 4 | open Microsoft.Extensions.Logging 5 | 6 | /// A custom logger that captures everything in memory and sends events via WebSockets to the connect debug tool. 7 | type InMemoryLogger = 8 | new : unit -> InMemoryLogger 9 | interface ILogger 10 | 11 | /// Start a Suave webserver to view all the logs inside the Fable.Daemon process. 12 | val startWebserver : logger : InMemoryLogger -> cancellationToken : CancellationToken -> Async 13 | -------------------------------------------------------------------------------- /Fable.Daemon/Fable.Daemon.fsproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | net8.0 6 | LatestMajor 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /Fable.Daemon/MSBuild.fs: -------------------------------------------------------------------------------- 1 | module Fable.Daemon.MSBuild 2 | 3 | open System 4 | open System.IO 5 | open System.Diagnostics 6 | open System.Reflection 7 | open Microsoft.Extensions.Logging 8 | 9 | /// Same as `dotnet_msbuild` but includes the defines as environment variables. 10 | let dotnet_msbuild_with_defines 11 | (logger : ILogger) 12 | (fsproj : FileInfo) 13 | (args : string) 14 | (defines : string list) 15 | : Async 16 | = 17 | backgroundTask { 18 | let psi = ProcessStartInfo "dotnet" 19 | let pwd = Assembly.GetEntryAssembly().Location |> Path.GetDirectoryName 20 | psi.WorkingDirectory <- pwd 21 | psi.Arguments <- $"msbuild \"%s{fsproj.FullName}\" %s{args}" 22 | psi.RedirectStandardOutput <- true 23 | psi.RedirectStandardError <- true 24 | psi.UseShellExecute <- false 25 | psi.EnvironmentVariables.["DOTNET_NOLOGO"] <- "1" 26 | 27 | if not (List.isEmpty defines) then 28 | let definesValue = defines |> String.concat ";" 29 | psi.Environment.Add ("DefineConstants", definesValue) 30 | 31 | use ps = new Process () 32 | ps.StartInfo <- psi 33 | ps.Start () |> ignore 34 | let output = ps.StandardOutput.ReadToEnd () 35 | let error = ps.StandardError.ReadToEnd () 36 | let ranToCompletion = ps.WaitForExit (TimeSpan.FromSeconds 5.) 37 | 38 | if not ranToCompletion then 39 | logger.LogCritical ( 40 | "dotnet msbuild \"{fsproj}\" {args}\n did not run until completion in time.", 41 | fsproj.FullName, 42 | args 43 | ) 44 | 45 | if not (String.IsNullOrWhiteSpace error) then 46 | logger.LogCritical ("dotnet msbuild \"{fsproj}\" {args}\n did has {error}", fsproj.FullName, args, error) 47 | failwithf $"In %s{pwd}:\ndotnet msbuild \"%s{fsproj.FullName}\" %s{args} failed with\n%s{error}" 48 | 49 | return output.Trim () 50 | } 51 | |> Async.AwaitTask 52 | 53 | /// Execute `dotnet msbuild` process and capture the stdout. 54 | /// Expected usage is with `--getProperty` and `--getItem` arguments. 55 | let dotnet_msbuild (logger : ILogger) (fsproj : FileInfo) (args : string) : Async = 56 | dotnet_msbuild_with_defines logger fsproj args List.empty 57 | -------------------------------------------------------------------------------- /Fable.Daemon/Program.fs: -------------------------------------------------------------------------------- 1 | open System 2 | open System.Diagnostics 3 | open System.IO 4 | open System.Threading 5 | open System.Text.Json 6 | open System.Text.Json.Serialization 7 | open System.Threading.Tasks 8 | open Microsoft.Extensions.Logging 9 | open Microsoft.Extensions.Logging.Abstractions 10 | open StreamJsonRpc 11 | open Fable 12 | open FSharp.Compiler.CodeAnalysis 13 | open FSharp.Compiler.SourceCodeServices 14 | open FSharp.Compiler.Diagnostics 15 | open Fable.Compiler.ProjectCracker 16 | open Fable.Compiler.Util 17 | open Fable.Compiler 18 | open Fable.Daemon 19 | 20 | type Msg = 21 | | ProjectChanged of payload : ProjectChangedPayload * AsyncReplyChannel 22 | | CompileFullProject of AsyncReplyChannel 23 | | CompileFiles of fileNames : string list * AsyncReplyChannel 24 | | Disconnect 25 | 26 | /// Input for every getFullProjectOpts 27 | /// Should be reused for subsequent type checks. 28 | type CrackerInput = 29 | { 30 | CliArgs : CliArgs 31 | /// Reuse the cracker options in future design time builds 32 | CrackerOptions : CrackerOptions 33 | } 34 | 35 | type Model = 36 | { 37 | CoolCatResolver : CoolCatResolver 38 | Checker : InteractiveChecker 39 | CrackerInput : CrackerInput option 40 | CrackerResponse : CrackerResponse 41 | SourceReader : SourceReader 42 | PathResolver : PathResolver 43 | TypeCheckProjectResult : TypeCheckProjectResult 44 | } 45 | 46 | let timeAsync f = 47 | async { 48 | let sw = Stopwatch.StartNew () 49 | let! result = f 50 | sw.Stop () 51 | return result, sw.Elapsed 52 | } 53 | 54 | type TypeCheckedProjectData = 55 | { 56 | TypeCheckProjectResult : TypeCheckProjectResult 57 | CrackerInput : CrackerInput 58 | Checker : InteractiveChecker 59 | CrackerResponse : CrackerResponse 60 | SourceReader : SourceReader 61 | /// An array of files that influence the design time build 62 | /// If any of these change, the plugin should respond accordingly. 63 | DependentFiles : FullPath array 64 | } 65 | 66 | let tryTypeCheckProject 67 | (logger : ILogger) 68 | (model : Model) 69 | (payload : ProjectChangedPayload) 70 | : Async> 71 | = 72 | async { 73 | try 74 | /// Project file will be in the Vite normalized format 75 | let projectFile = Path.GetFullPath payload.Project 76 | logger.LogDebug ("start tryTypeCheckProject for {projectFile}", projectFile) 77 | 78 | let cliArgs, crackerOptions = 79 | match model.CrackerInput with 80 | | Some { 81 | CliArgs = cliArgs 82 | CrackerOptions = crackerOptions 83 | } -> cliArgs, crackerOptions 84 | | None -> 85 | 86 | let cliArgs : CliArgs = 87 | { 88 | ProjectFile = projectFile 89 | RootDir = Path.GetDirectoryName payload.Project 90 | OutDir = None 91 | IsWatch = false 92 | Precompile = false 93 | PrecompiledLib = None 94 | PrintAst = false 95 | FableLibraryPath = Some payload.FableLibrary 96 | Configuration = payload.Configuration 97 | NoRestore = true 98 | NoCache = true 99 | NoParallelTypeCheck = false 100 | SourceMaps = false 101 | SourceMapsRoot = None 102 | Exclude = List.ofArray payload.Exclude 103 | Replace = Map.empty 104 | CompilerOptions = 105 | { 106 | TypedArrays = false 107 | ClampByteArrays = false 108 | Language = Language.JavaScript 109 | Define = [ "FABLE_COMPILER" ; "FABLE_COMPILER_4" ; "FABLE_COMPILER_JAVASCRIPT" ] 110 | DebugMode = false 111 | OptimizeFSharpAst = false 112 | Verbosity = Verbosity.Verbose 113 | // We keep using `.fs` for the compiled FSharp file, even though the contents will be JavaScript. 114 | FileExtension = ".fs" 115 | TriggeredByDependency = false 116 | NoReflection = payload.NoReflection 117 | } 118 | RunProcess = None 119 | Verbosity = Verbosity.Verbose 120 | } 121 | 122 | cliArgs, CrackerOptions (cliArgs, true) 123 | 124 | let crackerResponse = getFullProjectOpts model.CoolCatResolver crackerOptions 125 | logger.LogDebug ("CrackerResponse: {crackerResponse}", crackerResponse) 126 | let checker = InteractiveChecker.Create crackerResponse.ProjectOptions 127 | 128 | let sourceReader = 129 | Fable.Compiler.File.MakeSourceReader ( 130 | Array.map Fable.Compiler.File crackerResponse.ProjectOptions.SourceFiles 131 | ) 132 | |> snd 133 | 134 | let! typeCheckResult, typeCheckTime = 135 | timeAsync (CodeServices.typeCheckProject sourceReader checker cliArgs crackerResponse) 136 | 137 | logger.LogDebug ("Typechecking {projectFile} took {elapsed}", projectFile, typeCheckTime) 138 | 139 | let dependentFiles = 140 | model.CoolCatResolver.MSBuildProjectFiles projectFile 141 | |> List.map (fun fi -> fi.FullName) 142 | |> List.toArray 143 | 144 | return 145 | Ok 146 | { 147 | TypeCheckProjectResult = typeCheckResult 148 | CrackerInput = 149 | Option.defaultValue 150 | { 151 | CliArgs = cliArgs 152 | CrackerOptions = crackerOptions 153 | } 154 | model.CrackerInput 155 | Checker = checker 156 | CrackerResponse = crackerResponse 157 | SourceReader = sourceReader 158 | DependentFiles = dependentFiles 159 | } 160 | with ex -> 161 | logger.LogCritical ("tryTypeCheckProject threw exception {ex}", ex) 162 | return Error ex.Message 163 | } 164 | 165 | type CompiledProjectData = 166 | { 167 | CompiledFSharpFiles : Map 168 | } 169 | 170 | let private mapRange (m : FSharp.Compiler.Text.range) = 171 | { 172 | StartLine = m.StartLine 173 | StartColumn = m.StartColumn 174 | EndLine = m.EndLine 175 | EndColumn = m.EndColumn 176 | } 177 | 178 | let private mapDiagnostics (ds : FSharpDiagnostic array) = 179 | ds 180 | |> Array.map (fun d -> 181 | { 182 | ErrorNumberText = d.ErrorNumberText 183 | Message = d.Message 184 | Range = mapRange d.Range 185 | Severity = string d.Severity 186 | FileName = d.FileName 187 | } 188 | ) 189 | 190 | let tryCompileProject (logger : ILogger) (model : Model) : Async> = 191 | async { 192 | try 193 | let cachedFableModuleFiles = 194 | model.CoolCatResolver.TryGetCachedFableModuleFiles model.CrackerResponse.ProjectOptions.ProjectFileName 195 | 196 | let files = 197 | let cachedFiles = cachedFableModuleFiles.Keys |> Set.ofSeq 198 | 199 | model.CrackerResponse.ProjectOptions.SourceFiles 200 | |> Array.filter (fun sf -> 201 | not (sf.EndsWith (".fsi", StringComparison.Ordinal)) 202 | && not (cachedFiles.Contains sf) 203 | ) 204 | 205 | match model.CrackerInput with 206 | | None -> 207 | logger.LogCritical "tryCompileProject is entered without CrackerInput" 208 | return raise (exn "tryCompileProject is entered without CrackerInput") 209 | | Some { CliArgs = cliArgs } -> 210 | 211 | let! initialCompileResponse = 212 | CodeServices.compileMultipleFilesToJavaScript 213 | model.PathResolver 214 | cliArgs 215 | model.CrackerResponse 216 | model.TypeCheckProjectResult 217 | files 218 | 219 | if cachedFableModuleFiles.IsEmpty then 220 | let fableModuleFiles = 221 | initialCompileResponse.CompiledFiles 222 | |> Map.filter (fun key _value -> key.Contains "fable_modules") 223 | 224 | model.CoolCatResolver.WriteCachedFableModuleFiles 225 | model.CrackerResponse.ProjectOptions.ProjectFileName 226 | fableModuleFiles 227 | 228 | let compiledFiles = 229 | (initialCompileResponse.CompiledFiles, cachedFableModuleFiles) 230 | ||> Map.fold (fun state key value -> Map.add key value state) 231 | 232 | return Ok { CompiledFSharpFiles = compiledFiles } 233 | with ex -> 234 | logger.LogCritical ("tryCompileProject threw exception {ex}", ex) 235 | return Error ex.Message 236 | } 237 | 238 | type CompiledFileData = 239 | { 240 | CompiledFiles : Map 241 | Diagnostics : FSharpDiagnostic array 242 | } 243 | 244 | /// Find all the dependent files as efficient as possible. 245 | let rec getDependentFiles 246 | (sourceReader : SourceReader) 247 | (projectOptions : FSharpProjectOptions) 248 | (checker : InteractiveChecker) 249 | (inputFiles : string list) 250 | (result : Set) 251 | : Async> 252 | = 253 | async { 254 | match inputFiles with 255 | | [] -> 256 | // Filter out the signature files at the end. 257 | return 258 | result 259 | |> Set.filter (fun f -> not (f.EndsWith (".fsi", StringComparison.Ordinal))) 260 | | head :: tail -> 261 | 262 | // If the file is already part of the collection, it can safely be skipped. 263 | if result.Contains head then 264 | return! getDependentFiles sourceReader projectOptions checker tail result 265 | else 266 | 267 | let! nextFiles = checker.GetDependentFiles (head, projectOptions.SourceFiles, sourceReader) 268 | let nextResult = (result, nextFiles) ||> Array.fold (fun acc f -> Set.add f acc) 269 | 270 | return! getDependentFiles sourceReader projectOptions checker tail nextResult 271 | } 272 | 273 | let tryCompileFiles 274 | (logger : ILogger) 275 | (model : Model) 276 | (fileNames : string list) 277 | : Async> 278 | = 279 | async { 280 | try 281 | let fileNames = List.map Path.normalizePath fileNames 282 | logger.LogDebug ("tryCompileFile {fileNames}", fileNames) 283 | 284 | match model.CrackerInput with 285 | | None -> 286 | logger.LogCritical "tryCompileFile is entered without CrackerInput" 287 | return raise (exn "tryCompileFile is entered without CrackerInput") 288 | | Some { CliArgs = cliArgs } -> 289 | 290 | // Choose the signature file in the pair if it exists. 291 | let mapLeadingFile (file : string) : string = 292 | if file.EndsWith (".fsi", StringComparison.Ordinal) then 293 | file 294 | else 295 | model.CrackerResponse.ProjectOptions.SourceFiles 296 | |> Array.tryFind (fun f -> f = String.Concat (file, "i")) 297 | |> Option.defaultValue file 298 | 299 | let sourceReader = 300 | Fable.Compiler.File.MakeSourceReader ( 301 | Array.map Fable.Compiler.File model.CrackerResponse.ProjectOptions.SourceFiles 302 | ) 303 | |> snd 304 | 305 | let! filesToCompile = 306 | let input = List.map mapLeadingFile fileNames 307 | getDependentFiles sourceReader model.CrackerResponse.ProjectOptions model.Checker input Set.empty 308 | 309 | logger.LogDebug ("About to compile {allFiles}", filesToCompile) 310 | 311 | // Type-check the project up until the last file 312 | let lastFile = 313 | model.CrackerResponse.ProjectOptions.SourceFiles 314 | |> Array.tryFindBack filesToCompile.Contains 315 | |> Option.defaultValue (Array.last model.CrackerResponse.ProjectOptions.SourceFiles) 316 | 317 | let! checkProjectResult = 318 | model.Checker.ParseAndCheckProject ( 319 | cliArgs.ProjectFile, 320 | model.CrackerResponse.ProjectOptions.SourceFiles, 321 | sourceReader, 322 | lastFile = lastFile 323 | ) 324 | 325 | let! compiledFileResponse = 326 | Fable.Compiler.CodeServices.compileMultipleFilesToJavaScript 327 | model.PathResolver 328 | cliArgs 329 | model.CrackerResponse 330 | { model.TypeCheckProjectResult with 331 | ProjectCheckResults = checkProjectResult 332 | } 333 | filesToCompile 334 | 335 | return 336 | Ok 337 | { 338 | CompiledFiles = compiledFileResponse.CompiledFiles 339 | Diagnostics = compiledFileResponse.Diagnostics 340 | } 341 | with ex -> 342 | logger.LogCritical ("tryCompileFile threw exception {ex}", ex) 343 | return Error ex.Message 344 | } 345 | 346 | type FableServer(sender : Stream, reader : Stream, logger : ILogger) as this = 347 | let jsonMessageFormatter = new SystemTextJsonFormatter () 348 | 349 | do 350 | jsonMessageFormatter.JsonSerializerOptions <- 351 | let options = 352 | JsonSerializerOptions (PropertyNamingPolicy = JsonNamingPolicy.CamelCase) 353 | 354 | let jsonFSharpOptions = 355 | JsonFSharpOptions.Default().WithUnionTagName("case").WithUnionFieldsName ("fields") 356 | 357 | options.Converters.Add (JsonUnionConverter jsonFSharpOptions) 358 | options 359 | 360 | let cts = new CancellationTokenSource () 361 | 362 | do 363 | match logger with 364 | | :? Debug.InMemoryLogger as logger -> 365 | let server = Debug.startWebserver logger cts.Token 366 | Async.Start (server, cts.Token) 367 | | _ -> () 368 | 369 | let handler = 370 | new HeaderDelimitedMessageHandler (sender, reader, jsonMessageFormatter) 371 | 372 | let rpc : JsonRpc = new JsonRpc (handler, this) 373 | do rpc.StartListening () 374 | 375 | let mailbox = 376 | MailboxProcessor.Start (fun inbox -> 377 | let rec loop (model : Model) = 378 | async { 379 | let! msg = inbox.Receive () 380 | 381 | match msg with 382 | | ProjectChanged (payload, replyChannel) -> 383 | let! result = tryTypeCheckProject logger model payload 384 | 385 | match result with 386 | | Error error -> 387 | replyChannel.Reply (ProjectChangedResult.Error error) 388 | return! loop model 389 | | Ok result -> 390 | 391 | replyChannel.Reply ( 392 | ProjectChangedResult.Success ( 393 | result.CrackerResponse.ProjectOptions.SourceFiles, 394 | mapDiagnostics result.TypeCheckProjectResult.ProjectCheckResults.Diagnostics, 395 | result.DependentFiles 396 | ) 397 | ) 398 | 399 | return! 400 | loop 401 | { model with 402 | CrackerInput = Some result.CrackerInput 403 | Checker = result.Checker 404 | CrackerResponse = result.CrackerResponse 405 | SourceReader = result.SourceReader 406 | TypeCheckProjectResult = result.TypeCheckProjectResult 407 | } 408 | 409 | | CompileFullProject replyChannel -> 410 | let! result = tryCompileProject logger model 411 | 412 | match result with 413 | | Error error -> 414 | replyChannel.Reply (FilesCompiledResult.Error error) 415 | return! loop model 416 | | Ok result -> 417 | replyChannel.Reply (FilesCompiledResult.Success result.CompiledFSharpFiles) 418 | 419 | return! loop model 420 | 421 | | CompileFiles (fileNames, replyChannel) -> 422 | let! result = tryCompileFiles logger model fileNames 423 | 424 | match result with 425 | | Error error -> replyChannel.Reply (FileChangedResult.Error error) 426 | | Ok result -> 427 | replyChannel.Reply ( 428 | FileChangedResult.Success (result.CompiledFiles, mapDiagnostics result.Diagnostics) 429 | ) 430 | 431 | return! loop model 432 | | Disconnect -> return () 433 | } 434 | 435 | loop 436 | { 437 | CoolCatResolver = CoolCatResolver logger 438 | Checker = Unchecked.defaultof 439 | CrackerResponse = Unchecked.defaultof 440 | SourceReader = Unchecked.defaultof 441 | PathResolver = 442 | { new PathResolver with 443 | member _.TryPrecompiledOutPath (_sourceDir, _relativePath) = None 444 | member _.GetOrAddDeduplicateTargetDir (importDir, addTargetDir) = importDir 445 | } 446 | TypeCheckProjectResult = Unchecked.defaultof 447 | CrackerInput = None 448 | } 449 | ) 450 | 451 | // log or something. 452 | let subscription = mailbox.Error.Subscribe (fun evt -> ()) 453 | 454 | interface IDisposable with 455 | member _.Dispose () = 456 | if not (isNull subscription) then 457 | subscription.Dispose () 458 | 459 | if not cts.IsCancellationRequested then 460 | cts.Cancel () 461 | 462 | () 463 | 464 | /// returns a hot task that resolves when the stream has terminated 465 | member this.WaitForClose = rpc.Completion 466 | 467 | [] 468 | member _.ProjectChanged (p : ProjectChangedPayload) : Task = 469 | task { 470 | logger.LogDebug ("enter \"fable/project-changed\" {p}", p) 471 | let! response = mailbox.PostAndAsyncReply (fun replyChannel -> Msg.ProjectChanged (p, replyChannel)) 472 | logger.LogDebug ("exit \"fable/project-changed\" {response}", response) 473 | return response 474 | } 475 | 476 | [] 477 | member _.InitialCompile () : Task = 478 | task { 479 | logger.LogDebug "enter \"fable/initial-compile\"" 480 | let! response = mailbox.PostAndAsyncReply Msg.CompileFullProject 481 | 482 | let logResponse = 483 | match response with 484 | | FilesCompiledResult.Error e -> box e 485 | | FilesCompiledResult.Success result -> result.Keys |> String.concat "\n" |> sprintf "\n%s" |> box 486 | 487 | logger.LogDebug ("exit \"fable/initial-compile\" with {logResponse}", logResponse) 488 | return response 489 | } 490 | 491 | [] 492 | member _.CompileFiles (p : CompileFilesPayload) : Task = 493 | task { 494 | logger.LogDebug ("enter \"fable/compile\" with {p}", p) 495 | 496 | let! response = 497 | mailbox.PostAndAsyncReply (fun replyChannel -> 498 | Msg.CompileFiles (List.ofArray p.FileNames, replyChannel) 499 | ) 500 | 501 | let logResponse = 502 | match response with 503 | | FileChangedResult.Error e -> box e 504 | | FileChangedResult.Success (result, diagnostics) -> 505 | let keys = result.Keys |> String.concat "\n" |> sprintf "\n%s" 506 | box (keys, diagnostics) 507 | 508 | logger.LogDebug ("exit \"fable/compile\" with {p}", logResponse) 509 | return response 510 | } 511 | 512 | let input = Console.OpenStandardInput () 513 | let output = Console.OpenStandardOutput () 514 | 515 | let logger : ILogger = 516 | let envVar = Environment.GetEnvironmentVariable "VITE_PLUGIN_FABLE_DEBUG" 517 | 518 | if not (String.IsNullOrWhiteSpace envVar) && not (envVar = "0") then 519 | Debug.InMemoryLogger () 520 | else 521 | NullLogger.Instance 522 | 523 | // Set Fable logger 524 | Log.setLogger Verbosity.Verbose logger 525 | 526 | let daemon = 527 | new FableServer (Console.OpenStandardOutput (), Console.OpenStandardInput (), logger) 528 | 529 | AppDomain.CurrentDomain.ProcessExit.Add (fun _ -> (daemon :> IDisposable).Dispose ()) 530 | daemon.WaitForClose.GetAwaiter().GetResult () 531 | exit 0 532 | -------------------------------------------------------------------------------- /Fable.Daemon/README.md: -------------------------------------------------------------------------------- 1 | # Fable.Daemon 2 | 3 | This project uses JSON-RPC (JSON Remote Procedure Call) for communication between the main vite plugin interface [index.js](../index.js) and the [F# daemon](./Program.fs). 4 | 5 | The architecture enables efficient bi-directional communication, allowing features like hot reloading and incremental compilation of `.fs` source files and `.fsproj`. 6 | 7 | --- 8 | 9 | ## JRPC Server (.NET) 10 | 11 | The F# daemon sets up a JSON-RPC server using the `StreamJsonRpc` library. The server listens for incoming RPC calls and processes them. 12 | 13 | ## Methods 14 | 15 | ### 1. `fable/project-changed` 16 | - **Purpose**: Handles project configuration and changes via [Project Cracking](./CoolCatCracking.fs) to analyze and **extract metadata** from `.fsproj` files. 17 | - **Input**: Project configuration payload. 18 | - **Output**: Source files, diagnostics, and dependent files. 19 | 20 | ### 2. `fable/initial-compile` 21 | - **Purpose**: Performs the initial compilation of the entire project. 22 | - **Input**: None. 23 | - **Output**: Compiled F# files as JavaScript. 24 | 25 | ### 3. `fable/compile` 26 | - **Purpose**: Handles incremental compilation of changed files and HMR. 27 | - **Input**: Changed files payload. 28 | - **Output**: Compiled JavaScript for the changed F# files. 29 | 30 | 31 | #### Communication Streams 32 | The daemon communicates over standard input/output streams: 33 | 34 | ```fsharp 35 | let daemon = new FableServer( 36 | Console.OpenStandardOutput(), 37 | Console.OpenStandardInput(), 38 | logger 39 | ) 40 | ``` 41 | 42 | ## JRPC Client (JavaScript) 43 | 44 | The [Vite plugin (index.js)](../index.js) acts as the JSON-RPC client, making calls to the F# daemon using the exposed endpoints/methods. 45 | 46 | ### Example RPC Call 47 | 48 | ```js 49 | const result = await state.endpoint.send("fable/project-changed", { 50 | configuration: state.configuration, 51 | project: state.fsproj, 52 | fableLibrary, 53 | exclude: state.config.exclude, 54 | noReflection: state.config.noReflection 55 | }); 56 | ``` 57 | 58 | ## Architecture Benefits 59 | 60 | - **Incremental Compilation**: The `fable/compile` method enables incremental compilation, allowing for fast updates during development. 61 | - **Efficiency**: Communication over standard input/output streams ensures low overhead. 62 | - **Extensibility**: The JSON-RPC architecture allows for easy addition of new methods and features. 63 | 64 | 65 | -------------------------------------------------------------------------------- /Fable.Daemon/Types.fs: -------------------------------------------------------------------------------- 1 | namespace Fable.Daemon 2 | 3 | open FSharp.Compiler.CodeAnalysis 4 | 5 | type FullPath = string 6 | type Hash = string 7 | type JavaScript = string 8 | 9 | type ProjectChangedPayload = 10 | { 11 | /// Release or Debug. 12 | Configuration : string 13 | /// Absolute path of fsproj. 14 | Project : FullPath 15 | /// Absolute path of fable-library. Typically found in the npm modules. 16 | FableLibrary : FullPath 17 | /// Which project should be excluded? Used when you are testing a local plugin. 18 | Exclude : string array 19 | /// Don't emit JavaScript reflection code. 20 | NoReflection : bool 21 | } 22 | 23 | type DiagnosticRange = 24 | { 25 | StartLine : int 26 | StartColumn : int 27 | EndLine : int 28 | EndColumn : int 29 | } 30 | 31 | type Diagnostic = 32 | { 33 | ErrorNumberText : string 34 | Message : string 35 | Range : DiagnosticRange 36 | Severity : string 37 | FileName : FullPath 38 | } 39 | 40 | [] 41 | type ProjectChangedResult = 42 | | Success of sourceFiles : FullPath array * diagnostics : Diagnostic array * dependentFiles : FullPath array 43 | | Error of string 44 | 45 | [] 46 | type FilesCompiledResult = 47 | | Success of compiledFSharpFiles : Map 48 | | Error of string 49 | 50 | [] 51 | type FileChangedResult = 52 | | Success of compiledFSharpFiles : Map * diagnostics : Diagnostic array 53 | | Error of string 54 | 55 | type CompileFilesPayload = { FileNames : FullPath array } 56 | -------------------------------------------------------------------------------- /Fable.Daemon/debug/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fable-compiler/vite-plugin-fable/7248db2136fe9c0b62cdc0e44cc66f93c03db91c/Fable.Daemon/debug/favicon.ico -------------------------------------------------------------------------------- /Fable.Daemon/debug/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | vite-plugin-fable | debug tool 9 | 10 | 84 | 85 | 86 | 89 |
    90 | 106 | 107 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vite plugin for Fable 2 | 3 | [![NPM Version](https://img.shields.io/npm/v/vite-plugin-fable)](https://www.npmjs.com/package/vite-plugin-fable) 4 | 5 | 6 | vite-plugin-fable logo 7 | 8 | > [!IMPORTANT] 9 | > This project is up for adoption. I'm looking for eager people to maintain this.
    Please open a [discussion](https://github.com/fable-compiler/vite-plugin-fable/discussions) if you are interested! 10 | 11 | ## What is this? 12 | 13 | Alright, the `tl;dr` is that I don't like the current way of how you can use [Fable](https://fable.io) with [Vite](https://vitejs.dev). 14 | I'm referring to the steps in the [get started with Vite](https://fable.io/docs/getting-started/javascript.html#browser), I don't like it and have an alternate take on it. 15 | 16 | More thoughts on this can be read from the [documentation](https://fable.io/vite-plugin-fable/). 17 | 18 | ## Current status 19 | 20 | A first package was pushed to npm. This was merely to reserve the package name. 21 | You can read the code, that's is it for now. 22 | 23 | ### Recent Notes 24 | Support for the latest .NET runtime was added in [v0.1.1](https://github.com/fable-compiler/vite-plugin-fable/blob/main/CHANGELOG.md#011---2025-06-03). Please upgrade to the latest version. Earlier versions may fail silently if the .NET 8 runtime is missing—see the changelog for details. 25 | 26 | ## Video 27 | 28 | I talked a little bit about this project during this stream: 29 | 30 | [![vite-plugin-fable stream](http://img.youtube.com/vi/nVpUaVFNpMk/maxresdefault.jpg)](https://youtu.be/mnqwwtSQfRU?si=VpDDv3SzHikXL5iu&t=141 "vite-plugin-fable") 31 | -------------------------------------------------------------------------------- /changelog-updater.js: -------------------------------------------------------------------------------- 1 | import { $ } from "bun"; 2 | 3 | // Updates the package.json version according to latest release from CHANGELOG.md 4 | // https://www.npmjs.com/package/keep-a-changelog#cli 5 | 6 | const version = await $`bunx changelog --latest-release`.text(); 7 | 8 | const packageVersion = await $`npm info vite-plugin-fable version`.text(); 9 | 10 | if (version === packageVersion) { 11 | process.exit(0); 12 | } 13 | 14 | await $`npm version ${version.trim()}`; -------------------------------------------------------------------------------- /cracking.fsx: -------------------------------------------------------------------------------- 1 | #I "./bin" 2 | #r "Fable.AST" 3 | #r "Fable.Compiler" 4 | #r "Fable.Daemon" 5 | #r "./bin/FSharp.Compiler.Service.dll" 6 | #r "./bin/Microsoft.Extensions.Logging.Abstractions.dll" 7 | 8 | open System.IO 9 | open Microsoft.Extensions.Logging 10 | open Fable.Compiler.Util 11 | open Fable.Compiler.ProjectCracker 12 | open Fable.Daemon 13 | 14 | fsi.AddPrinter (fun (x : ProjectOptionsResponse) -> 15 | $"ProjectOptionsResponse: %i{x.ProjectOptions.Length} options, %i{x.ProjectReferences.Length} references, %s{x.TargetFramework.Value}, %s{x.OutputType.Value}" 16 | ) 17 | 18 | let fsproj = 19 | // @"C:\Users\nojaf\Projects\telplin\tool\client\OnlineTool.fsproj" 20 | // "/home/nojaf/projects/fantomas-tools/src/client/fsharp/FantomasTools.fsproj" 21 | Path.Combine (__SOURCE_DIRECTORY__, "sample-project/App.fsproj") 22 | |> Path.GetFullPath 23 | 24 | let cliArgs : CliArgs = 25 | { 26 | ProjectFile = fsproj 27 | RootDir = __SOURCE_DIRECTORY__ 28 | OutDir = None 29 | IsWatch = false 30 | Precompile = false 31 | PrecompiledLib = None 32 | PrintAst = false 33 | FableLibraryPath = Some (Path.Combine (__SOURCE_DIRECTORY__, "sample-project/node_modules/fable-library")) 34 | Configuration = "Debug" 35 | NoRestore = false 36 | NoCache = true 37 | NoParallelTypeCheck = false 38 | SourceMaps = false 39 | SourceMapsRoot = None 40 | Exclude = [] 41 | Replace = Map.empty 42 | RunProcess = None 43 | CompilerOptions = 44 | { 45 | TypedArrays = true 46 | ClampByteArrays = false 47 | Language = Fable.Language.JavaScript 48 | Define = [ "FABLE_COMPILER" ; "FABLE_COMPILER_4" ; "FABLE_COMPILER_JAVASCRIPT" ] 49 | DebugMode = true 50 | OptimizeFSharpAst = false 51 | Verbosity = Fable.Verbosity.Verbose 52 | FileExtension = ".fs" 53 | TriggeredByDependency = false 54 | NoReflection = false 55 | } 56 | Verbosity = Fable.Verbosity.Verbose 57 | } 58 | 59 | let options : CrackerOptions = CrackerOptions (cliArgs, true) 60 | 61 | let logger = 62 | { new ILogger with 63 | member x.Log<'TState> 64 | ( 65 | logLevel : LogLevel, 66 | _eventId : EventId, 67 | state : 'TState, 68 | ex : exn, 69 | formatter : System.Func<'TState, exn, string> 70 | ) 71 | : unit 72 | = 73 | let level = string logLevel 74 | printfn $"%s{level}: %s{formatter.Invoke (state, ex)}" 75 | 76 | member x.BeginScope<'TState> (_state : 'TState) : System.IDisposable = null 77 | member x.IsEnabled (_logLevel : LogLevel) : bool = true 78 | } 79 | 80 | let resolver : ProjectCrackerResolver = CoolCatResolver logger 81 | 82 | #time "on" 83 | 84 | let result = resolver.GetProjectOptionsFromProjectFile (true, options, fsproj) 85 | 86 | #time "off" 87 | 88 | // result.ProjectReferences 89 | 90 | for option in result.ProjectOptions do 91 | printfn "%s" option 92 | 93 | 94 | open System 95 | 96 | DateTime.Now.ToString ("HH:mm:ss.fff") 97 | -------------------------------------------------------------------------------- /docs/_body.html: -------------------------------------------------------------------------------- 1 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /docs/_head.html: -------------------------------------------------------------------------------- 1 | 10 | -------------------------------------------------------------------------------- /docs/content/fsdocs-theme.css: -------------------------------------------------------------------------------- 1 | @import url("https://fonts.googleapis.com/css2?family=Mohave:wght@400;600&display=swap"); 2 | 3 | :root { 4 | --nojaf-50: #fefee8; 5 | --nojaf-100: #feffc2; 6 | --nojaf-200: #fffe87; 7 | --nojaf-300: #fff643; 8 | --nojaf-400: #ffe710; 9 | --nojaf-500: #efce03; 10 | --nojaf-600: #cea100; 11 | --nojaf-700: #a47304; 12 | --nojaf-800: #885a0b; 13 | --nojaf-900: #734910; 14 | --nojaf-950: #432605; 15 | --fable-50: #edf9ff; 16 | --fable-100: #d7f1ff; 17 | --fable-200: #b9e8ff; 18 | --fable-300: #88dbff; 19 | --fable-400: #50c5ff; 20 | --fable-500: #28a7ff; 21 | --fable-600: #1e90ff; 22 | --fable-700: #0a71eb; 23 | --fable-800: #0f5abe; 24 | --fable-900: #134e95; 25 | --fable-950: #11305a; 26 | 27 | --mohave: Mohave, sans-serif; 28 | --header-background: var(--fable-50); 29 | --aside-background: var(--fable-50); 30 | --page-menu-background-color: var(--fable-50); 31 | --header-brand-text-transform: uppercase; 32 | --heading-font-family: var(--mohave); 33 | } 34 | 35 | [data-theme="dark"] { 36 | --header-background: var(--fable-900); 37 | --aside-background: var(--fable-900); 38 | --page-menu-background-color: var(--fable-900); 39 | --heading-color: white; 40 | } 41 | 42 | header .start strong { 43 | font-family: var(--heading-font-family); 44 | font-size: var(--font-400); 45 | font-weight: 600; 46 | } 47 | 48 | #content { 49 | padding-bottom: var(--spacing-400); 50 | } 51 | 52 | #content > p:last-child:has(a) { 53 | margin-top: 0; 54 | display: flex; 55 | 56 | > a:only-child { 57 | margin-top: var(--spacing-400); 58 | margin-inline: auto; 59 | padding: var(--spacing-50) var(--spacing-200); 60 | background-color: var(--fable-600); 61 | text-decoration: none; 62 | color: white; 63 | transition: 300ms all; 64 | font-family: var(--mohave); 65 | 66 | &:hover { 67 | background-color: var(--fable-700); 68 | } 69 | 70 | &:active { 71 | background-color: var(--fable-800); 72 | } 73 | } 74 | } 75 | 76 | .mermaid { 77 | margin-block: var(--spacing-500); 78 | display: flex; 79 | justify-content: center; 80 | 81 | & .messageText { 82 | fill: var(--fable-400) !important; 83 | } 84 | 85 | & .messageLine0 { 86 | stroke: var(--fable-400) !important; 87 | } 88 | 89 | & text.actor > tspan { 90 | fill: white !important; 91 | } 92 | 93 | & .activation0 { 94 | fill: var(--fable-200) !important; 95 | stroke: var(--fable-300) !important; 96 | } 97 | } 98 | 99 | vpf-command { 100 | display: block; 101 | margin-block: var(--spacing-400); 102 | } 103 | 104 | blockquote { 105 | margin-inline: 0; 106 | } 107 | 108 | blockquote > p { 109 | font-weight: 600; 110 | font-size: var(--font-500); 111 | font-family: var(--monospace-font); 112 | } 113 | -------------------------------------------------------------------------------- /docs/debug.md: -------------------------------------------------------------------------------- 1 | --- 2 | index: 6 3 | categoryindex: 1 4 | category: docs 5 | --- 6 | 7 | # Debugging 8 | 9 | This plugin is a wonderful piece until it no longer works. Then it just sucks, and who knows where in the rabbit hole the problem lies. 10 | The biggest fear this plugin can face is when the dotnet process no longer responds. 11 | Even if it is not able to process the incoming request, it should always be able to produce a response. 12 | 13 | ## Debug viewer 14 | 15 | In order to find out what happened in the dotnet world, you can set the `VITE_PLUGIN_FABLE_DEBUG` environment variable before running Vite. 16 | 17 | ```bash 18 | # bash 19 | export VITE_PLUGIN_FABLE_DEBUG=1 20 | ``` 21 | 22 | ```pwsh 23 | # PowerShell 24 | $env:VITE_PLUGIN_FABLE_DEBUG=1 25 | ``` 26 | 27 | When running Vite, you should see something like this in the Vite output: 28 | 29 | ```shell 30 | Running daemon in debug mode, visit http://localhost:9014 to view logs 31 | ``` 32 | 33 | Opening [http://localhost:9014](http://localhost:9014) will display a list of log messages that happened inside the dotnet process: 34 | 35 | ![vite-plugin-fable debug tool](./img/debug-tool.png) 36 | 37 | It should receive new log messages via web sockets after the initial page load. 38 | 39 | [Next]({{fsdocs-next-page-link}}) 40 | -------------------------------------------------------------------------------- /docs/getting-started.md: -------------------------------------------------------------------------------- 1 | --- 2 | index: 2 3 | categoryindex: 1 4 | category: docs 5 | --- 6 | 7 | # Getting started 8 | 9 | ## Create a new Vite project 10 | 11 | First you need a new Vite project: 12 | 13 | 14 | 15 | ## Add the plugin 16 | 17 | Next, you need to install the `vite-plugin-fable` package: 18 | 19 | 20 | 21 | It is important that the _post-install script_ of the plugin did run. The first time this runs, it can take some time. 22 | 23 | If for some reason it didn't run, please manually invoke: 24 | 25 | 26 | 27 | _Note: you don't need to install Fable as a dotnet tool when using this plugin._ 28 | 29 | ## Update your Vite configuration 30 | 31 | Lastly, we need to tell Vite to use our plugin: 32 | 33 | ```js 34 | import { defineConfig } from "vite"; 35 | import fable from "vite-plugin-fable"; 36 | 37 | // https://vitejs.dev/config/ 38 | export default defineConfig({ 39 | plugins: [fable()], 40 | }); 41 | ``` 42 | 43 | ⚠️ Depending on your use-case, you may need to further tweak your configuration. 44 | Check out the [recipes](./recipes.md) page for more tips. 45 | 46 | ## Start Vite 47 | 48 | We can now start the Vite dev server: 49 | 50 | 51 | 52 | You should see a bunch of logs: 53 | 54 | ``` 55 | 12:32:42 PM [vite] [fable]: configResolved: Configuration: Debug 56 | 12:32:42 PM [vite] [fable]: configResolved: Entry fsproj /home/projects/your-project-folder/App.fsproj 57 | 12:32:42 PM [vite] [fable]: buildStart: Starting daemon 58 | 12:32:42 PM [vite] [fable]: buildStart: Initial project file change! 59 | 12:32:42 PM [vite] [fable]: projectChanged: dependent file /home/projects/your-project-folder/App.fsproj changed. 60 | 12:32:42 PM [vite] [fable]: compileProject: Full compile started of /home/projects/your-project-folder/App.fsproj 61 | 12:32:42 PM [vite] [fable]: compileProject: fable-library located at /home/projects/your-project-folder/node_modules/@fable-org/fable-library-js 62 | 12:32:42 PM [vite] [fable]: compileProject: about to type-checked /home/projects/your-project-folder/App.fsproj. 63 | 12:32:44 PM [vite] [fable]: compileProject: /home/projects/your-project-folder/App.fsproj was type-checked. 64 | 12:32:44 PM [vite] [fable]: compileProject: Full compile completed of /home/projects/your-project-folder/App.fsproj 65 | ``` 66 | 67 | And now we can import our code from F# in our `index.html`: 68 | 69 | ```html 70 | 73 | ``` 74 | 75 | ⚠️ We cannot use `` because Vite won't recognize the `.fs` extension outside the module resolution. 76 | See [vitejs/vite#9981](https://github.com/vitejs/vite/pull/9981) 77 | 78 | [Next]({{fsdocs-next-page-link}}) 79 | -------------------------------------------------------------------------------- /docs/how.md: -------------------------------------------------------------------------------- 1 | --- 2 | index: 3 3 | categoryindex: 1 4 | category: docs 5 | --- 6 | 7 | # How does this work? 8 | 9 | The key concept is that everything starts with launching the Vite dev server and a special Vite plugin is able to deal with importing an F# file. 10 | 11 | Assuming you have an existing `fsproj`, with all your code and F# dependencies, your `vite.config.js` would need to wire up the plugin: 12 | 13 | ```js 14 | import { defineConfig } from "vite"; 15 | import fable from "vite-plugin-fable"; 16 | 17 | // https://vitejs.dev/config/ 18 | export default defineConfig({ 19 | plugins: [fable()], 20 | }); 21 | ``` 22 | 23 | [Vite plugins](https://vitejs.dev/plugins/) have various hooks that will deal with importing and transforming F# files. 24 | These hooks will start a `dotnet` process and communicate with it via [JSON RPC](https://www.jsonrpc.org/). 25 | 26 | ## Index.html 27 | 28 | The index.html needs to import an F# file to the plugin to process: 29 | 30 | ```html 31 | 32 | 33 | 34 | 35 | 36 | 37 | Vite + Fable 38 | 39 | 40 | 43 | 44 | 45 | ``` 46 | 47 | The is a technical limitation why we cannot load the initial entrypoint as ``. 48 | See [vitejs/vite#9981](https://github.com/vitejs/vite/pull/9981) 49 | 50 | ## Starting Vite 51 | 52 | npm run dev 53 | 54 |
    55 | sequenceDiagram 56 | Vite->>dotnet: 1. config resolve hook 57 | activate dotnet 58 | dotnet->>Vite: 2. FSharpOptions resolved 59 | deactivate dotnet 60 | Vite->>dotnet: 3. FSharp file changed 61 | activate dotnet 62 | dotnet->>Vite: 4. Transformed FSharp files 63 | deactivate dotnet 64 |
    65 | 66 | ### Config resolution 67 | 68 | When the Vite dev server starts it needs to process the main `fsproj` file and do the initial compile of all F# files to JavaScript. 69 | All the good stuff happens here: 70 | 71 | - NuGet packages get resolved. 72 | - The [FSharpProjectOptions](https://fsharp.github.io/fsharp-compiler-docs/reference/fsharp-compiler-codeanalysis-fsharpprojectoptions.html) is being composed. 73 | - The project gets type-checked and Fable will transpile all source files to JavaScript. 74 | 75 | The `dotnet` process is using [Fable.Compiler](https://github.com/fable-compiler/Fable/issues/3552) to pull this off. 76 | This is a large portion of shared code that also would be running when you invoke `dotnet fable`. 77 | 78 | ### FSharpOptions resolved 79 | 80 | The resulting JSON message the dotnet process will return to the Vite plugin are the FSharpOptions and the entire set of compiled F# files. 81 | 82 | Note that no transpiled F# file has been written to disk at this point. We keep everything in memory and avoid IO overhead. 83 | 84 | ### An FSharp file changed 85 | 86 | When we edit our code, we need to re-evaluate our current project. One or multiple F# files could potentially need a recompile to JavaScript. 87 | The [Graph Based checking](https://devblogs.microsoft.com/dotnet/a-new-fsharp-compiler-feature-graphbased-typechecking/) algorithm is used here to figure out what needs to be reevaluated. 88 | 89 | Note that we let Vite decide here whether any file was changed or not. This is already a key difference between how this setup works and how `dotnet fable` works. 90 | 91 | ### Transformed FSharp files 92 | 93 | The JSON rpc response will contain the latest version of the changed files. The plugin will update the inner state and the `handleHotUpdate` hook will alert the browser if any file change requires an HMR update or browser reload. 94 | 95 | [Next]({{fsdocs-next-page-link}}) 96 | -------------------------------------------------------------------------------- /docs/img/debug-tool.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fable-compiler/vite-plugin-fable/7248db2136fe9c0b62cdc0e44cc66f93c03db91c/docs/img/debug-tool.png -------------------------------------------------------------------------------- /docs/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fable-compiler/vite-plugin-fable/7248db2136fe9c0b62cdc0e44cc66f93c03db91c/docs/img/favicon.ico -------------------------------------------------------------------------------- /docs/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fable-compiler/vite-plugin-fable/7248db2136fe9c0b62cdc0e44cc66f93c03db91c/docs/img/logo.png -------------------------------------------------------------------------------- /docs/implications.md: -------------------------------------------------------------------------------- 1 | --- 2 | index: 4 3 | categoryindex: 1 4 | category: docs 5 | --- 6 | 7 | # Technical implications 8 | 9 | When working on this plugin, there are a lot of moving pieces involved: 10 | 11 | - vite-plugin-fable 12 | - Fable.Compiler 13 | - the F# fork Fable uses 14 | 15 | All these pieces of technologies need to be in tune before any of this can work. 16 | 17 | ## vite-plugin-fable 18 | 19 | The combination of a thin JavaScript wrapped that communicates with a dotnet process via JSON-RPC. 20 | 21 | ### index.js 22 | 23 | This Vite plugin harnesses the power of both [Vite specific hooks](https://vitejs.dev/guide/api-plugin.html#rollup-plugin-compatibility) and [Rollup hooks](https://rollupjs.org/plugin-development/). 24 | While both offer documentation, occasionally you might find it doesn't cover every scenario. 25 | In those moments, a bit of trial and error becomes part of the adventure. 26 | Alternatively, reaching out to the vibrant community on the Vite Discord can shine a light on the trickier parts. 27 | 28 | ### Fable.Daemon 29 | 30 | The `dotnet` process leverages [StreamJsonRpc](https://github.com/Microsoft/vs-streamjsonrpc) alongside a mailbox processor for managing incoming requests. 31 | The art lies in ensuring a proper response is always dispatched, even when facing the unexpected, like an issue in the user code. 32 | 33 | ## Fable.Compiler 34 | 35 | `Fable.Compiler` emerges from a [carve-out](https://github.com/fable-compiler/Fable/pull/3656) of the codebase originally part of `Fable.Cli`. 36 | It's worth noting that this NuGet package is in its early stages, crafted specifically for this explorative phase. 37 | Navigating this terrain often involves simultaneous tweaks in both this project and `Fable.Daemon`. 38 | To streamline this process, you can set `true` in the `Directory.Build.props` file, ensuring a smoother development experience. 39 | 40 | ### Trivia 41 | 42 | - Transpiled F# files are not written to disk but are kept in memory. 43 | - The transpiled JavaScript maintains references to the original F# files for imports, e.g., `import { Foo } from "./Bar.fs"`. This approach informs the Vite plugin that it's working with virtual files. 44 | - Presently, only F# is supported, mainly due to the lack of a pressing need to incorporate other languages. 45 | 46 | ## NCave's F# Fork 47 | 48 | Fable takes a unique path by not relying on the official releases of the [F# compiler](https://fsharp.github.io/fsharp-compiler-docs/), sidestepping what you might have installed in your SDK or found on NuGet. 49 | Instead, it embraces [a specialized fork](https://github.com/ncave/fsharp/pull/2) crafted by an enigmatic figure known as NCave. 50 | A little piece of fun trivia: the true identity of NCave remains a mystery, and there's a notable silence between the Fable and F# teams. 51 | So, if you had preconceived notions about their collaboration, consider this a gentle nudge towards the bleak reality. 52 | 53 | On a personal note, I've contributed [a handful of PRs](https://github.com/ncave/fsharp/pulls?q=is%3Apr+is%3Aclosed+author%3Anojaf) to NCave's fork, specifically to harness the graph-based checking algorithm. 54 | 55 | ## Conclusion 56 | 57 | Contributing to this project presents a significant challenge, necessitating a broad understanding of the aforementioned topics. 58 | This isn't by design but simply reflects the complex nature of the project. 59 | 60 | [Next]({{fsdocs-next-page-link}}) 61 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | index: 1 3 | categoryindex: 1 4 | category: docs 5 | --- 6 | 7 | # vite-plugin-fable 8 | 9 | 10 | 11 | ![vite-plugin-fable logo](./img/logo.png) 12 | 13 | > This project is up for adoption. I'm looking for eager people to maintain this.
    14 | > Please open a [discussion](https://github.com/fable-compiler/vite-plugin-fable/discussions) if you are interested! 15 | 16 | ## Introduction 17 | 18 | When diving into Vite, I found myself having a friendly debate with what the [get started with Vite](https://fable.io/docs/getting-started/javascript.html#browser) guide suggests. 19 | It's purely a matter of taste, and I mean no disrespect to the authors. 20 | 21 | If you peek at the latest Fable docs, you'll notice this snippet at the end: 22 | 23 | dotnet fable watch --run npx vite 24 | 25 | Now, that's where my preferences raise an eyebrow. 26 | For nearly everything else in Vite, whether it's JSX, TypeScript, Sass, or Markdown, I find myself typing `npm run dev`. (or even `bun run dev`, cheers to [Bun](https://twitter.com/i/status/1701702174810747346) for that!) 27 | You know, the command that summons `vite`, the proclaimed [Next Generation Frontend Tooling](https://vitejs.dev/). 28 | 29 | I'm of the opinion (_brace yourselves, hot take incoming_) that integrating Fable with frontend development should align as closely as possible with the broader ecosystem. Vite is a star in the frontend universe, with a user base dwarfing that of F# developers. It makes sense to harmonize with their flow, not the other way around. 30 | 31 | bun run dev 32 | 33 | I absolutely recognize and respect the legacy of Fable. It's a veteran in the scene, predating Vite, so I get the historical reasons for its current approach. 34 | But that doesn't mean I can't cheer for evolution and a bit of change, right? 35 | 36 | [Next]({{fsdocs-next-page-link}}) 37 | -------------------------------------------------------------------------------- /docs/local-fable-compiler.md: -------------------------------------------------------------------------------- 1 | --- 2 | index: 7 3 | categoryindex: 1 4 | category: docs 5 | --- 6 | 7 | # Using a Local Fable Compiler 8 | 9 | It is relatively easy to set up a local Fable compiler for troubleshooting the plugin. 10 | 11 | ## Checkout the Fable Repository 12 | 13 | Clone the [Fable repository](https://github.com/fable-compiler/Fable) as a sibling to this repository. 14 | 15 | ## Using Local Fable Binaries 16 | 17 | Set `true` in [Directory.Build.props](https://github.com/fable-compiler/vite-plugin-fable/blob/main/Directory.Build.props). After running `bun install`, the daemon will be built using the local binary. 18 | 19 | ## Use Local fable-library-js 20 | 21 | Sometimes, there could be changes in `@fable-org/fable-library-js` that you need to reflect in the daemon's output. 22 | 23 | Build the fable-library using `./build.sh fable-library --javascript` (from the Fable repository root). 24 | 25 | Update the `package.json` (in the root) to: 26 | 27 | ```json 28 | { 29 | "dependencies": { 30 | "@fable-org/fable-library-js": "../Fable/temp/fable-library-js" 31 | } 32 | } 33 | ``` 34 | 35 | Install again using `bun install`. 36 | 37 | [Next]({{fsdocs-next-page-link}}) 38 | -------------------------------------------------------------------------------- /docs/recipes.md: -------------------------------------------------------------------------------- 1 | --- 2 | index: 5 3 | categoryindex: 1 4 | category: docs 5 | --- 6 | 7 | # Recipes 8 | 9 | There are a few things you can configure in the plugin configuration. 10 | 11 | ## Alternative fsproj 12 | 13 | By default, the plugin will look for a single `.fsproj` file inside the folder that holds your `vite.config.js` file. 14 | If you deviate from this setup you can specify the entry `fsproj` file: 15 | 16 | ```js 17 | import path from "node:path"; 18 | import { fileURLToPath } from "node:url"; 19 | import { defineConfig } from "vite"; 20 | import fable from "vite-plugin-fable"; 21 | 22 | const currentDir = path.dirname(fileURLToPath(import.meta.url)); 23 | const fsproj = path.join(currentDir, "fsharp/FantomasTools.fsproj"); 24 | 25 | // https://vitejs.dev/config/ 26 | export default defineConfig({ 27 | plugins: [fable({ fsproj })], 28 | }); 29 | ``` 30 | 31 | ## Using React 32 | 33 | There are a couple of ways to deal with React and JSX in Fable. 34 | 35 | ⚠️ When using the `vite-plugin-fable` in combination with `@vitejs/plugin-react`, you do want to specify the fable plugin first! ⚠️ 36 | 37 | ### Feliz.CompilerPlugins 38 | 39 | If you are using [Feliz.CompilerPlugins](https://www.nuget.org/packages/Feliz.CompilerPlugins), Fable output React Classic Runtime code. 40 | Stuff like `React.createElement`. You will need to tailor your `@vitejs/plugin-react` accordingly: 41 | 42 | ```js 43 | import { defineConfig } from "vite"; 44 | import react from "@vitejs/plugin-react"; 45 | import fable from "vite-plugin-fable"; 46 | 47 | // https://vitejs.dev/config/ 48 | export default defineConfig({ 49 | plugins: [ 50 | fable(), 51 | react({ include: /\.(fs|js|jsx|ts|tsx)$/, jsxRuntime: "classic" }), 52 | ], 53 | }); 54 | ``` 55 | 56 | Note that the `react` plugin will only apply the fast-refresh wrapper when you specify the `fs` extension in the `include`. 57 | 58 | ### Fable.Core.JSX 59 | 60 | Fable can also produce JSX (see [blog](https://fable.io/blog/2022/2022-10-12-react-jsx.html)). In this case, you need to tell the `fable` plugin it should transform the JSX using Babel. 61 | The `@vitejs/plugin-react` won't interact with any `.fs` files, so that transformation needs to happen in the `fable` plugin: 62 | 63 | ```js 64 | import { defineConfig } from "vite"; 65 | import react from "@vitejs/plugin-react"; 66 | import fable from "vite-plugin-fable"; 67 | 68 | // https://vitejs.dev/config/ 69 | export default defineConfig({ 70 | plugins: [ 71 | // See `jsx` option from https://esbuild.github.io/api/#transformation 72 | fable({ jsx: "automatic" }), 73 | react({ include: /\.(fs|js|jsx|ts|tsx)$/ }), 74 | ], 75 | }); 76 | ``` 77 | 78 | Note that you will still need to tweak the `react` plugin with `include` to enable the fast refresh transformation. 79 | 80 | ### Plain Fable.React 81 | 82 | If you are for some reason using [Fable.React](https://www.nuget.org/packages/Fable.React) without [Feliz.CompilerPlugins](https://www.nuget.org/packages/Feliz.CompilerPlugins), there is one gotcha to get fast refresh working. 83 | 84 | `Fable.React` will use the [old JSX output](https://legacy.reactjs.org/blog/2020/09/22/introducing-the-new-jsx-transform.html). 85 | The `@vitejs/plugin-react` needs to respect that in the configuration: 86 | 87 | ```js 88 | import react from "@vitejs/plugin-react"; 89 | import fable from "vite-plugin-fable"; 90 | 91 | // https://vitejs.dev/config/ 92 | export default defineConfig({ 93 | plugins: [ 94 | fable(), 95 | react({ include: /\.(fs|js|jsx|ts|tsx)$/, jsxRuntime: "classic" }), 96 | ], 97 | }); 98 | ``` 99 | 100 | However, this is not enough for the fast refresh wrapper to be added. 101 | ⚠️ The React plugin will **specifically** look for a `import React from "react"` statement. 102 | 103 | ```fsharp 104 | module Component 105 | 106 | open Fable.React 107 | open Fable.React.Props 108 | 109 | // Super important for fast refresh to work in "classic" mode. 110 | // The [] attribute from Feliz.CompilerPlugin will add for you. 111 | // But here, we are not using that and we need to add this ourselves. 112 | Fable.Core.JsInterop.emitJsStatement () "import React from \"react\"" 113 | 114 | let App () = 115 | let counterHook = Hooks.useState(0) 116 | 117 | div [] [ 118 | h1 [] [ str "Hey you!" ] 119 | p [] [ 120 | ofInt counterHook.current 121 | ] 122 | button [ OnClick (fun _ -> counterHook.update(fun c -> c + 1))] [ 123 | str "Increase" 124 | ] 125 | ] 126 | ``` 127 | 128 | [Next]({{fsdocs-next-page-link}}) 129 | -------------------------------------------------------------------------------- /docs/scripts/command.js: -------------------------------------------------------------------------------- 1 | import { LitElement, html, css } from "lit"; 2 | import copy from "copy-to-clipboard"; 3 | 4 | const COMMAND_STORAGE_KEY = "vpf_command"; 5 | 6 | class Command extends LitElement { 7 | static properties = { 8 | // {state: true} means Lit sees these class properties as internal. 9 | // Note that this is meta info and is not the same thing as this._value down below. 10 | _value: { state: true }, 11 | _clicked: { state: true }, 12 | }; 13 | 14 | constructor() { 15 | super(); 16 | /** @type String */ 17 | this._value = ""; 18 | /** @type Boolean */ 19 | this._clicked = false; 20 | this.onValueChanged = this.onValueChanged.bind(this); 21 | } 22 | 23 | connectedCallback() { 24 | super.connectedCallback(); 25 | window.addEventListener(COMMAND_STORAGE_KEY, this.onValueChanged); 26 | } 27 | 28 | disconnectedCallback() { 29 | super.disconnectedCallback(); 30 | window.removeEventListener(COMMAND_STORAGE_KEY, this.onValueChanged); 31 | } 32 | 33 | static styles = css` 34 | div { 35 | background-color: var(--code-background); 36 | border-radius: var(--radius); 37 | } 38 | ul { 39 | display: flex; 40 | list-style: none; 41 | margin: 0; 42 | padding: 0 var(--spacing-200); 43 | gap: var(--spacing-400); 44 | border-bottom: 1px solid var(--header-border); 45 | cursor: pointer; 46 | } 47 | 48 | ul li { 49 | padding-block: var(--spacing-100); 50 | transition: 200ms all; 51 | border-bottom: 2px solid transparent; 52 | } 53 | 54 | ul li:hover { 55 | border-bottom: 2px solid var(--fable-400); 56 | } 57 | 58 | ul li.active { 59 | border-bottom: 2px solid var(--fable-600); 60 | font-weight: 500; 61 | } 62 | 63 | main { 64 | font-family: var(--monospace-font); 65 | display: flex; 66 | align-items: center; 67 | justify-content: space-between; 68 | gap: var(--spacing-200); 69 | padding: var(--spacing-200); 70 | } 71 | 72 | main div { 73 | display: flex; 74 | align-items: center; 75 | gap: var(--spacing-200); 76 | } 77 | 78 | main > iconify-icon { 79 | cursor: pointer; 80 | transition: 200ms all; 81 | } 82 | 83 | main > iconify-icon:hover { 84 | color: var(--fable-700); 85 | } 86 | `; 87 | 88 | /** 89 | * @param {String} value 90 | */ 91 | onOptionClick(value) { 92 | localStorage.setItem(COMMAND_STORAGE_KEY, value); 93 | const event = new CustomEvent(COMMAND_STORAGE_KEY, { 94 | detail: value, 95 | }); 96 | window.dispatchEvent(event); 97 | } 98 | 99 | /** @param ev {CustomEvent} */ 100 | onValueChanged(ev) { 101 | this._value = ev.detail; 102 | } 103 | 104 | /** @param {String} contents */ 105 | copyToClipboard(contents) { 106 | copy(contents); 107 | this._clicked = true; 108 | setTimeout(() => { 109 | this._clicked = false; 110 | }, 500); 111 | } 112 | 113 | render() { 114 | const attributes = [...this.attributes]; 115 | 116 | if (attributes.length === 0) { 117 | return null; 118 | } 119 | 120 | const activeIndex = (() => { 121 | const stored = localStorage.getItem(COMMAND_STORAGE_KEY); 122 | if (!stored) { 123 | this._value = attributes[0].name; 124 | localStorage.setItem(COMMAND_STORAGE_KEY, this._value); 125 | return 0; 126 | } else { 127 | this.value = stored; 128 | return attributes.findIndex((a) => a.name === stored); 129 | } 130 | })(); 131 | const command = attributes[activeIndex] && attributes[activeIndex].value; 132 | 133 | const copyElement = this._clicked 134 | ? html`
    135 | copied! 136 | 141 |
    ` 142 | : html` { 147 | this.copyToClipboard(command); 148 | }} 149 | >`; 150 | 151 | return html` 152 |
    153 |
      154 | ${attributes.map((a, i) => { 155 | const className = i === activeIndex ? "active" : ""; 156 | return html`
    • this.onOptionClick(a.name)} 159 | > 160 | ${a.name} 161 |
    • `; 162 | })} 163 |
    164 |
    165 |
    166 | 171 | ${command} 172 |
    173 | ${copyElement} 174 |
    175 |
    176 | `; 177 | } 178 | } 179 | 180 | customElements.define("vpf-command", Command); 181 | -------------------------------------------------------------------------------- /docs/status.md: -------------------------------------------------------------------------------- 1 | --- 2 | index: 8 3 | categoryindex: 1 4 | category: docs 5 | --- 6 | 7 | # Current status 8 | 9 | At present, this project is my personal playground. Like any open-source initiative, 10 | the code is open for perusal – and that's the primary expectation to set. 11 | 12 | ## What works? 13 | 14 | So far, I've managed to run and build my [Telplin project](https://github.com/nojaf/telplin/) locally. 15 | It's a promising start, though it's not yet primed for broader use. 16 | This project does support Fable plugins, but compatibility with other projects isn't assured. 17 | 18 | ## What is published? 19 | 20 | A first package was pushed to [npm](https://www.npmjs.com/package/vite-plugin-fable). This was merely to reserve the package name. 21 | 22 | ## Own aspirations 23 | 24 | I'm looking to incorporate this into a few of my ventures, like [Fantomas tools](https://fsprojects.github.io/fantomas-tools/), 25 | and I'm intrigued by the potential integration with Astro projects and the inclusion of simple scripts. 26 | 27 | ## The roadmap 28 | 29 | This project is a sporadic labor of love. Progress is steady but unhurried, with significant intervals between updates. 30 | I am committed to seeing this through, convinced of its substantial value. 31 | Yet, there's an air of 'memento mori' – it's a passion project that could fade at any moment. 32 | 33 | ## Everything else 34 | 35 | Heads up, folks! The GitHub issue tracker is off the grid. Why, you ask? This isn't just another code repository. 36 | It's a journey, a chunk of my life carved into digital form. Sure, the code's there for you, open and free. 37 | But my time? That's a different story. I've poured years into these skills, building, breaking, learning. 38 | So, I'm channeling my energy into the code, not endless chats. 39 | 40 | Got something sharp, a genuine gem of a contribution? I'm all about that. 41 | Throw a PR my way. Let's get the conversation rolling with some real, in-depth code talk. 42 | Show me you've got the game, that you're not just passing through. 43 | 44 | And if you're thinking, "Hey, I need this guy on my side," then we're talking business, my friend. 45 | [Hit me up](https://nojaf.com/) for some contracting work. That's the signal to me that you're serious, ready to invest in making something awesome together. 46 | 47 |   48 | -------------------------------------------------------------------------------- /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { 3 | "version": "9.0.102", 4 | "rollForward": "latestMinor" 5 | } 6 | } -------------------------------------------------------------------------------- /ideas.md: -------------------------------------------------------------------------------- 1 | # Ideas 2 | 3 | Here, scattered thoughts dwell, crafted for none but my own soul's gaze. No vow within these words shall be bound to the morrow's light. 4 | 5 | - Filter out the diagnostics for files inside fable_modules. Probably configurable via plugin options. 6 | - Fable.Compiler should expose some sort of version property -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import { spawn } from "node:child_process"; 2 | import { fileURLToPath } from "node:url"; 3 | import { promises as fs } from "node:fs"; 4 | import path from "node:path"; 5 | import { JSONRPCEndpoint } from "ts-lsp-client"; 6 | import { normalizePath } from "vite"; 7 | import { transform } from "esbuild"; 8 | import { filter, map, bufferTime, Subject } from "rxjs"; 9 | import colors from "picocolors"; 10 | // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/withResolvers 11 | import withResolvers from "promise.withresolvers"; 12 | import { codeFrameColumns } from "@babel/code-frame"; 13 | 14 | withResolvers.shim(); 15 | 16 | const fsharpFileRegex = /\.(fs|fsx)$/; 17 | const currentDir = path.dirname(fileURLToPath(import.meta.url)); 18 | const fableDaemon = path.join(currentDir, "bin/Fable.Daemon.dll"); 19 | 20 | if (process.env.VITE_PLUGIN_FABLE_DEBUG) { 21 | console.log( 22 | `Running daemon in debug mode, visit http://localhost:9014 to view logs`, 23 | ); 24 | } 25 | 26 | /** 27 | * @typedef {Object} PluginOptions 28 | * @property {string} [fsproj] - The main fsproj to load 29 | * @property {'transform' | 'preserve' | 'automatic' | null} [jsx] - Apply JSX transformation after Fable compilation: https://esbuild.github.io/api/#transformation 30 | * @property {Boolean} [noReflection] - Pass noReflection value to Fable.Compiler 31 | * @property {string[]} [exclude] - Pass exclude to Fable.Compiler 32 | */ 33 | 34 | /** @type {PluginOptions} */ 35 | const defaultConfig = { jsx: null, noReflection: false, exclude: [] }; 36 | 37 | /** 38 | * @function 39 | * @param {PluginOptions} userConfig - The options for configuring the plugin. 40 | * @description Initializes and returns a Vite plugin for to process the incoming F# project. 41 | * @returns {import('vite').Plugin} A Vite plugin object with the standard structure and hooks. 42 | */ 43 | export default function fablePlugin(userConfig) { 44 | /** @type {import("./types.js").PluginState} */ 45 | const state = { 46 | config: Object.assign({}, defaultConfig, userConfig), 47 | compilableFiles: new Map(), 48 | sourceFiles: new Set(), 49 | fsproj: null, 50 | configuration: "Debug", 51 | dependentFiles: new Set(), 52 | // @ts-ignore 53 | logger: { info: console.log, warn: console.warn, error: console.error }, 54 | dotnetProcess: null, 55 | endpoint: null, 56 | pendingChanges: null, 57 | hotPromiseWithResolvers: null, 58 | isBuild: false, 59 | }; 60 | 61 | /** @type {Subject} **/ 62 | const pendingChangesSubject = new Subject(); 63 | 64 | /** 65 | * @param {String} prefix 66 | * @param {String} message 67 | */ 68 | function logDebug(prefix, message) { 69 | state.logger.info(colors.dim(`[fable]: ${prefix}: ${message}`), { 70 | timestamp: true, 71 | }); 72 | } 73 | 74 | /** 75 | * @param {String} prefix 76 | * @param {String} message 77 | */ 78 | function logInfo(prefix, message) { 79 | state.logger.info(colors.green(`[fable]: ${prefix}: ${message}`), { 80 | timestamp: true, 81 | }); 82 | } 83 | 84 | /** 85 | * @param {String} prefix 86 | * @param {String} message 87 | */ 88 | function logWarn(prefix, message) { 89 | state.logger.warn(colors.yellow(`[fable]: ${prefix}: ${message}`), { 90 | timestamp: true, 91 | }); 92 | } 93 | 94 | /** 95 | * @param {String} prefix 96 | * @param {String} message 97 | */ 98 | function logError(prefix, message) { 99 | state.logger.warn(colors.red(`[fable] ${prefix}: ${message}`), { 100 | timestamp: true, 101 | }); 102 | } 103 | 104 | /** 105 | * @param {String} prefix 106 | * @param {String} message 107 | */ 108 | function logCritical(prefix, message) { 109 | state.logger.error(colors.red(`[fable] ${prefix}: ${message}`), { 110 | timestamp: true, 111 | }); 112 | } 113 | 114 | /** 115 | @param {string} configDir - Folder path of the vite.config.js file. 116 | */ 117 | async function findFsProjFile(configDir) { 118 | const files = await fs.readdir(configDir); 119 | const fsprojFiles = files 120 | .filter((file) => file && file.toLocaleLowerCase().endsWith(".fsproj")) 121 | .map((fsProjFile) => { 122 | // Return the full path of the .fsproj file 123 | return normalizePath(path.join(configDir, fsProjFile)); 124 | }); 125 | return fsprojFiles.length > 0 ? fsprojFiles[0] : null; 126 | } 127 | 128 | /** 129 | @returns {Promise} 130 | */ 131 | async function getFableLibrary() { 132 | const fableLibraryInOwnNodeModules = path.join( 133 | currentDir, 134 | "node_modules/@fable-org/fable-library-js", 135 | ); 136 | try { 137 | await fs.access(fableLibraryInOwnNodeModules, fs.constants.F_OK); 138 | return normalizePath(fableLibraryInOwnNodeModules); 139 | } catch (e) { 140 | return normalizePath( 141 | path.join(currentDir, "../@fable-org/fable-library-js"), 142 | ); 143 | } 144 | } 145 | 146 | /** 147 | * Retrieves the project file. At this stage the project is type-checked but Fable did not compile anything. 148 | * @param {string} fableLibrary - Location of the fable-library node module. 149 | * @returns {Promise} A promise that resolves to an object containing the project options and compiled files. 150 | * @throws {Error} If the result from the endpoint is not a success case. 151 | */ 152 | async function getProjectFile(fableLibrary) { 153 | /** @type {import("./types.js").FSharpDiscriminatedUnion} */ 154 | const result = await state.endpoint.send("fable/project-changed", { 155 | configuration: state.configuration, 156 | project: state.fsproj, 157 | fableLibrary, 158 | exclude: state.config.exclude, 159 | noReflection: state.config.noReflection, 160 | }); 161 | 162 | if (result.case === "Success") { 163 | return { 164 | sourceFiles: result.fields[0], 165 | diagnostics: result.fields[1], 166 | dependentFiles: result.fields[2], 167 | }; 168 | } else { 169 | throw new Error(result.fields[0] || "Unknown error occurred"); 170 | } 171 | } 172 | 173 | /** 174 | * Try and compile the entire project using Fable. The daemon contains all the information at this point to do this. 175 | * No need to pass any additional info. 176 | * @returns {Promise>} A promise that resolves a map of compiled files. 177 | * @throws {Error} If the result from the endpoint is not a success case. 178 | */ 179 | async function tryInitialCompile() { 180 | /** @type {import("./types.js").FSharpDiscriminatedUnion} */ 181 | const result = await state.endpoint.send("fable/initial-compile"); 182 | 183 | if (result.case === "Success") { 184 | return result.fields[0]; 185 | } else { 186 | throw new Error(result.fields[0] || "Unknown error occurred"); 187 | } 188 | } 189 | 190 | /** 191 | * @function 192 | * @param {import("./types.js").Diagnostic} diagnostic 193 | * @returns {string} 194 | */ 195 | function formatDiagnostic(diagnostic) { 196 | return `${diagnostic.severity.toUpperCase()} ${diagnostic.errorNumberText}: ${diagnostic.message} ${diagnostic.fileName} (${diagnostic.range.startLine},${diagnostic.range.startColumn}) (${diagnostic.range.endLine},${diagnostic.range.endColumn})`; 197 | } 198 | 199 | /** 200 | * @function 201 | * @param {import("./types.js").Diagnostic[]} diagnostics - An array of Diagnostic objects to be logged. 202 | */ 203 | function logDiagnostics(diagnostics) { 204 | for (const diagnostic of diagnostics) { 205 | switch (diagnostic.severity.toLowerCase()) { 206 | case "error": 207 | logError("", formatDiagnostic(diagnostic)); 208 | break; 209 | case "warning": 210 | logWarn("", formatDiagnostic(diagnostic)); 211 | break; 212 | default: 213 | logInfo("", formatDiagnostic(diagnostic)); 214 | break; 215 | } 216 | } 217 | } 218 | 219 | /** 220 | * Does a type-check and compilation of the state.fsproj 221 | * @function 222 | * @param {function} addWatchFile 223 | * @returns {Promise} 224 | */ 225 | async function compileProject(addWatchFile) { 226 | logInfo("compileProject", `Full compile started of ${state.fsproj}`); 227 | const fableLibrary = await getFableLibrary(); 228 | logDebug("compileProject", `fable-library located at ${fableLibrary}`); 229 | logInfo("compileProject", `about to type-checked ${state.fsproj}.`); 230 | const projectResponse = await getProjectFile(fableLibrary); 231 | logInfo("compileProject", `${state.fsproj} was type-checked.`); 232 | logDiagnostics(projectResponse.diagnostics); 233 | for (const sf of projectResponse.sourceFiles) { 234 | state.sourceFiles.add(normalizePath(sf)); 235 | } 236 | for (let dependentFile of projectResponse.dependentFiles) { 237 | dependentFile = normalizePath(dependentFile); 238 | state.dependentFiles.add(dependentFile); 239 | addWatchFile(dependentFile); 240 | } 241 | const compiledFSharpFiles = await tryInitialCompile(); 242 | logInfo("compileProject", `Full compile completed of ${state.fsproj}`); 243 | state.sourceFiles.forEach((file) => { 244 | addWatchFile(file); 245 | const normalizedFileName = normalizePath(file); 246 | state.compilableFiles.set(normalizedFileName, compiledFSharpFiles[file]); 247 | }); 248 | } 249 | 250 | /** 251 | * Either the project or a dependent file changed 252 | * @returns {Promise} 253 | * @param {function} addWatchFile 254 | * @param {Set} projectFiles 255 | */ 256 | async function projectChanged(addWatchFile, projectFiles) { 257 | try { 258 | logInfo( 259 | "projectChanged", 260 | `dependent file ${Array.from(projectFiles).join("\n")} changed.`, 261 | ); 262 | state.sourceFiles.clear(); 263 | state.compilableFiles.clear(); 264 | state.dependentFiles.clear(); 265 | await compileProject(addWatchFile); 266 | } catch (e) { 267 | logCritical( 268 | "projectChanged", 269 | `Unexpected failure during projectChanged for ${Array.from(projectFiles)},\n${e}`, 270 | ); 271 | } 272 | } 273 | 274 | /** 275 | * F# files part of state.compilableFiles have changed. 276 | * @returns {Promise} 277 | * @param {String[]} files 278 | */ 279 | async function fsharpFileChanged(files) { 280 | try { 281 | /** @type {import("./types.js").FSharpDiscriminatedUnion} */ 282 | const compilationResult = await state.endpoint.send("fable/compile", { 283 | fileNames: files, 284 | }); 285 | if ( 286 | compilationResult.case === "Success" && 287 | compilationResult.fields && 288 | compilationResult.fields.length > 0 289 | ) { 290 | const compiledFSharpFiles = compilationResult.fields[0]; 291 | 292 | logDebug( 293 | "fsharpFileChanged", 294 | `\n${Object.keys(compiledFSharpFiles).join("\n")} compiled`, 295 | ); 296 | 297 | for (const [key, value] of Object.entries(compiledFSharpFiles)) { 298 | const normalizedFileName = normalizePath(key); 299 | state.compilableFiles.set(normalizedFileName, value); 300 | } 301 | 302 | const diagnostics = compilationResult.fields[1]; 303 | logDiagnostics(diagnostics); 304 | return diagnostics; 305 | } else { 306 | logError( 307 | "watchChange", 308 | `compilation of ${files} failed, ${compilationResult.fields[0]}`, 309 | ); 310 | return []; 311 | } 312 | } catch (e) { 313 | logCritical( 314 | "watchChange", 315 | `compilation of ${files} failed, plugin could not handle this gracefully. ${e}`, 316 | ); 317 | return []; 318 | } 319 | } 320 | 321 | /** 322 | * @param {import("./types.js").PendingChangesState} acc 323 | * @param {import("./types.js").HookEvent} e 324 | * @return {import("./types.js").PendingChangesState} 325 | */ 326 | function reducePendingChange(acc, e) { 327 | if (e.type === "FSharpFileChanged") { 328 | return { 329 | projectChanged: acc.projectChanged, 330 | fsharpFiles: acc.fsharpFiles.add(e.file), 331 | projectFiles: acc.projectFiles, 332 | }; 333 | } else if (e.type === "ProjectFileChanged") { 334 | return { 335 | projectChanged: true, 336 | fsharpFiles: acc.fsharpFiles, 337 | projectFiles: acc.projectFiles.add(e.file), 338 | }; 339 | } else { 340 | logWarn("pendingChanges", `Unexpected pending change ${e}`); 341 | return acc; 342 | } 343 | } 344 | 345 | /** 346 | * @param {import("./types.js").Diagnostic} diagnostic 347 | * @returns {Promise} 348 | */ 349 | async function makeHmrError(diagnostic) { 350 | const fileContent = await fs.readFile(diagnostic.fileName, "utf-8"); 351 | const frame = codeFrameColumns(fileContent, { 352 | start: { 353 | line: diagnostic.range.startLine, 354 | col: diagnostic.range.startColumn, 355 | }, 356 | end: { 357 | line: diagnostic.range.endLine, 358 | col: diagnostic.range.endColumn, 359 | }, 360 | }); 361 | return { 362 | type: "error", 363 | err: { 364 | message: diagnostic.message, 365 | frame: frame, 366 | stack: "", 367 | id: diagnostic.fileName, 368 | loc: { 369 | file: diagnostic.fileName, 370 | line: diagnostic.range.startLine, 371 | column: diagnostic.range.startColumn, 372 | }, 373 | }, 374 | }; 375 | } 376 | 377 | return { 378 | name: "vite-plugin-fable", 379 | enforce: "pre", 380 | configResolved: async function (resolvedConfig) { 381 | state.logger = resolvedConfig.logger; 382 | state.configuration = 383 | resolvedConfig.env.MODE === "production" ? "Release" : "Debug"; 384 | state.isBuild = resolvedConfig.command === "build"; 385 | logDebug("configResolved", `Configuration: ${state.configuration}`); 386 | const configDir = 387 | resolvedConfig.configFile && path.dirname(resolvedConfig.configFile); 388 | 389 | if (state.config && state.config.fsproj) { 390 | state.fsproj = state.config.fsproj; 391 | } else { 392 | state.fsproj = await findFsProjFile(configDir); 393 | } 394 | 395 | if (!state.fsproj) { 396 | logCritical( 397 | "configResolved", 398 | `No .fsproj file was found in ${configDir}`, 399 | ); 400 | } else { 401 | logInfo("configResolved", `Entry fsproj ${state.fsproj}`); 402 | } 403 | }, 404 | buildStart: async function (options) { 405 | try { 406 | logInfo("buildStart", "Starting daemon"); 407 | state.dotnetProcess = spawn("dotnet", [fableDaemon, "--stdio"], { 408 | shell: true, 409 | stdio: "pipe", 410 | }); 411 | state.endpoint = new JSONRPCEndpoint( 412 | state.dotnetProcess.stdin, 413 | state.dotnetProcess.stdout, 414 | ); 415 | 416 | if (state.isBuild) { 417 | await projectChanged( 418 | this.addWatchFile.bind(this), 419 | new Set([state.fsproj]), 420 | ); 421 | } else { 422 | state.pendingChanges = pendingChangesSubject 423 | .pipe( 424 | bufferTime(50), 425 | map((events) => { 426 | return events.reduce(reducePendingChange, { 427 | projectChanged: false, 428 | fsharpFiles: new Set(), 429 | projectFiles: new Set(), 430 | }); 431 | }), 432 | filter( 433 | (state) => state.projectChanged || state.fsharpFiles.size > 0, 434 | ), 435 | ) 436 | .subscribe(async (pendingChanges) => { 437 | let diagnostics = []; 438 | 439 | if (pendingChanges.projectChanged) { 440 | await projectChanged( 441 | this.addWatchFile.bind(this), 442 | pendingChanges.projectFiles, 443 | ); 444 | } else { 445 | const files = Array.from(pendingChanges.fsharpFiles); 446 | logDebug("subscribe", files.join("\n")); 447 | diagnostics = await fsharpFileChanged(files); 448 | } 449 | 450 | if (state.hotPromiseWithResolvers) { 451 | state.hotPromiseWithResolvers.resolve(diagnostics); 452 | state.hotPromiseWithResolvers = null; 453 | } 454 | }); 455 | 456 | logDebug("buildStart", "Initial project file change!"); 457 | state.hotPromiseWithResolvers = Promise.withResolvers(); 458 | pendingChangesSubject.next({ 459 | type: "ProjectFileChanged", 460 | file: state.fsproj, 461 | }); 462 | await state.hotPromiseWithResolvers.promise; 463 | } 464 | } catch (e) { 465 | logCritical("buildStart", `Unexpected failure during buildStart: ${e}`); 466 | } 467 | }, 468 | transform: async function (src, id) { 469 | if (fsharpFileRegex.test(id)) { 470 | logDebug("transform", id); 471 | if (state.compilableFiles.has(id)) { 472 | let code = state.compilableFiles.get(id); 473 | // If Fable outputted JSX, we still need to transform this. 474 | // @vitejs/plugin-react does not do this. 475 | if (state.config.jsx) { 476 | const esbuildResult = await transform(code, { 477 | loader: "jsx", 478 | jsx: state.config.jsx, 479 | }); 480 | code = esbuildResult.code; 481 | } 482 | return { 483 | code: code, 484 | map: null, 485 | }; 486 | } else { 487 | logWarn("transform", `${id} is not part of compilableFiles.`); 488 | } 489 | } 490 | }, 491 | watchChange: async function (id, change) { 492 | if (state.sourceFiles.size !== 0 && state.dependentFiles.has(id)) { 493 | pendingChangesSubject.next({ type: "ProjectFileChanged", file: id }); 494 | } 495 | }, 496 | handleHotUpdate: async function ({ file, server, modules }) { 497 | if (state.compilableFiles.has(file)) { 498 | logDebug("handleHotUpdate", `enter for ${file}`); 499 | pendingChangesSubject.next({ 500 | type: "FSharpFileChanged", 501 | file: file, 502 | }); 503 | 504 | // handleHotUpdate could be called concurrently because multiple files changed. 505 | if (!state.hotPromiseWithResolvers) { 506 | state.hotPromiseWithResolvers = Promise.withResolvers(); 507 | } 508 | 509 | // The idea is to wait for a shared promise to resolve. 510 | // This will resolve in the subscription of state.changedFSharpFiles 511 | const diagnostics = await state.hotPromiseWithResolvers.promise; 512 | logDebug("handleHotUpdate", `leave for ${file}`); 513 | 514 | const errorDiagnostic = diagnostics.find( 515 | (diag) => diag.severity === "Error", 516 | ); 517 | if (errorDiagnostic) { 518 | const msg = await makeHmrError(errorDiagnostic); 519 | console.log(msg); 520 | server.hot.send(msg); 521 | return []; 522 | } else { 523 | // Potentially a file that is not imported in the current graph was changed. 524 | // Vite should not try and hot update that module. 525 | return modules.filter((m) => m.importers.size !== 0); 526 | } 527 | } 528 | }, 529 | buildEnd: () => { 530 | logInfo("buildEnd", "Closing daemon"); 531 | if (state.dotnetProcess) { 532 | state.dotnetProcess.kill(); 533 | } 534 | if (state.pendingChanges) { 535 | state.pendingChanges.unsubscribe(); 536 | } 537 | }, 538 | }; 539 | } 540 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vite-plugin-fable", 3 | "version": "0.1.1", 4 | "homepage": "http://fable.io/vite-plugin-fable/", 5 | "description": "", 6 | "main": "index.js", 7 | "type": "module", 8 | "scripts": { 9 | "test": "echo \"Error: no test specified\" && exit 1", 10 | "format": "bunx --bun prettier ./package.json ./index.js ./types.d.ts ./sample-project/vite.config.js ./docs/*.md ./docs/content/*.css ./docs/scripts/*.js ./docs/*.html ./.devcontainer/devcontainer.json --write && dotnet fantomas .", 11 | "postinstall": "dotnet publish Fable.Daemon/Fable.Daemon.fsproj --nologo -c Release --ucr -p:PublishReadyToRun=true -o ./bin", 12 | "lint": "bunx tsc", 13 | "prepublishOnly": "bun changelog-updater.js" 14 | }, 15 | "files": [ 16 | "index.js", 17 | "cracking.fsx", 18 | "Directory.Build.props", 19 | "Directory.Packages.props", 20 | "Fable.Daemon/*.fsproj", 21 | "Fable.Daemon/**/*.fs", 22 | "Fable.Daemon/**/*.fsi", 23 | "Fable.Daemon/debug", 24 | "!Fable.Daemon/obj", 25 | "!Fable.Daemon/README.md" 26 | ], 27 | "keywords": [], 28 | "author": "nojaf", 29 | "license": "Apache-2.0", 30 | "fundinding": "https://nojaf.com/", 31 | "dependencies": { 32 | "@babel/code-frame": "^7.26.2", 33 | "@fable-org/fable-library-js": "2.0.0-beta.3", 34 | "promise.withresolvers": "^1.0.3", 35 | "rxjs": "^7.8.1", 36 | "ts-lsp-client": "^1.0.3" 37 | }, 38 | "peerDependencies": { 39 | "esbuild": "*", 40 | "vite": "^6.0.0" 41 | }, 42 | "devDependencies": { 43 | "@types/node": "^22.10.7", 44 | "copy-to-clipboard": "^3.3.3", 45 | "keep-a-changelog": "^2.6.2", 46 | "lit": "^3.2.1", 47 | "prettier": "3.4.2", 48 | "typescript": "5.7.3" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /sample-project/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | 26 | # Fable 27 | fable_modules/ -------------------------------------------------------------------------------- /sample-project/App.fsproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0 5 | true 6 | false 7 | false 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /sample-project/Component.fs: -------------------------------------------------------------------------------- 1 | module Components.Component 2 | 3 | open Fable.Core 4 | open React 5 | open type React.DSL.DOMProps 6 | open type React.React 7 | 8 | JsInterop.importSideEffects "./app.css" 9 | 10 | [] 11 | let Component () : JSX.Element = 12 | let count, setCount = useStateByFunction (0) 13 | 14 | fragment [] [ 15 | h1 [] [ str "React rocks!" ] 16 | button [ OnClick (fun _ -> setCount ((+) 1)) ] [ str $"Current state {count}" ] 17 | ] 18 | -------------------------------------------------------------------------------- /sample-project/Component.fsi: -------------------------------------------------------------------------------- 1 | module Components.Component 2 | 3 | open Fable.Core 4 | 5 | [] 6 | val Component : unit -> JSX.Element 7 | -------------------------------------------------------------------------------- /sample-project/Directory.Build.props: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /sample-project/Library.fs: -------------------------------------------------------------------------------- 1 | module App 2 | 3 | open Fable.Core 4 | open Browser.Dom 5 | open Math 6 | open Thoth.Json 7 | 8 | let r = sum 1 19 9 | 10 | let someJsonString = 11 | Encode.object [ "track", Encode.string "Changes" ] |> Encode.toString 4 12 | 13 | let h1Element = document.querySelector "h1" 14 | h1Element.textContent <- $"Dynamic Fable text %i{r}! %s{someJsonString}" 15 | 16 | open React 17 | 18 | let app = document.querySelector "#app" 19 | ReactDom.createRoot(app).render (JSX.create Components.Component.Component []) 20 | -------------------------------------------------------------------------------- /sample-project/Math.fs: -------------------------------------------------------------------------------- 1 | module Math 2 | 3 | let sum a b = a + 2 4 | -------------------------------------------------------------------------------- /sample-project/README.md: -------------------------------------------------------------------------------- 1 | # React + Vite 2 | 3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. 4 | 5 | Currently, two official plugins are available: 6 | 7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh 8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh 9 | -------------------------------------------------------------------------------- /sample-project/app.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Rockwell, 'Rockwell Nova', 'Roboto Slab', 'DejaVu Serif', 'Sitka Small', serif; 3 | font-weight: normal; 4 | padding: 30px; 5 | } 6 | 7 | h1 { 8 | font-weight: 400; 9 | font-size: 40px; 10 | } 11 | 12 | #app { 13 | margin: 60px auto; 14 | padding: 30px; 15 | background-color: lightgreen; 16 | display: block; 17 | max-width: 600px; 18 | border-radius: 6px; 19 | 20 | & h1 { 21 | margin: 0; 22 | margin-bottom: 60px; 23 | } 24 | 25 | & button { 26 | outline: 3px solid green; 27 | background-color: limegreen; 28 | color: white; 29 | border: none; 30 | border-radius: 6px; 31 | padding: 5px 10px; 32 | cursor: pointer; 33 | font-size: 20px; 34 | display: block; 35 | margin-inline: auto; 36 | 37 | &:hover { 38 | background-color: green; 39 | outline-color: darkgreen; 40 | transform: translateY(-2px); 41 | box-shadow: 0px 3px 3px 0px rgba(0,0,0,0.25); 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /sample-project/bun.lock: -------------------------------------------------------------------------------- 1 | { 2 | "lockfileVersion": 1, 3 | "workspaces": { 4 | "": { 5 | "name": "sample-project", 6 | "dependencies": { 7 | "@fable-org/fable-library-js": "^2.0.0-beta.3", 8 | "react": "^18.3.1", 9 | "react-dom": "^18.3.1", 10 | }, 11 | "devDependencies": { 12 | "@types/react": "^18.3.12", 13 | "@types/react-dom": "^18.3.1", 14 | "@vitejs/plugin-react": "4.3.4", 15 | "vite": "^6.0.1", 16 | "vite-plugin-inspect": "0.8.8", 17 | }, 18 | }, 19 | }, 20 | "packages": { 21 | "@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="], 22 | 23 | "@antfu/utils": ["@antfu/utils@0.7.10", "", {}, "sha512-+562v9k4aI80m1+VuMHehNJWLOFjBnXn3tdOitzD0il5b7smkSBal4+a3oKiQTbrwMmN/TBUMDvbdoWDehgOww=="], 24 | 25 | "@babel/code-frame": ["@babel/code-frame@7.26.2", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.25.9", "js-tokens": "^4.0.0", "picocolors": "^1.0.0" } }, "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ=="], 26 | 27 | "@babel/compat-data": ["@babel/compat-data@7.26.2", "", {}, "sha512-Z0WgzSEa+aUcdiJuCIqgujCshpMWgUpgOxXotrYPSA53hA3qopNaqcJpyr0hVb1FeWdnqFA35/fUtXgBK8srQg=="], 28 | 29 | "@babel/core": ["@babel/core@7.26.0", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.26.0", "@babel/generator": "^7.26.0", "@babel/helper-compilation-targets": "^7.25.9", "@babel/helper-module-transforms": "^7.26.0", "@babel/helpers": "^7.26.0", "@babel/parser": "^7.26.0", "@babel/template": "^7.25.9", "@babel/traverse": "^7.25.9", "@babel/types": "^7.26.0", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg=="], 30 | 31 | "@babel/generator": ["@babel/generator@7.26.2", "", { "dependencies": { "@babel/parser": "^7.26.2", "@babel/types": "^7.26.0", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^3.0.2" } }, "sha512-zevQbhbau95nkoxSq3f/DC/SC+EEOUZd3DYqfSkMhY2/wfSeaHV1Ew4vk8e+x8lja31IbyuUa2uQ3JONqKbysw=="], 32 | 33 | "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.25.9", "", { "dependencies": { "@babel/compat-data": "^7.25.9", "@babel/helper-validator-option": "^7.25.9", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-j9Db8Suy6yV/VHa4qzrj9yZfZxhLWQdVnRlXxmKLYlhWUVB1sB2G5sxuWYXk/whHD9iW76PmNzxZ4UCnTQTVEQ=="], 34 | 35 | "@babel/helper-module-imports": ["@babel/helper-module-imports@7.25.9", "", { "dependencies": { "@babel/traverse": "^7.25.9", "@babel/types": "^7.25.9" } }, "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw=="], 36 | 37 | "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.26.0", "", { "dependencies": { "@babel/helper-module-imports": "^7.25.9", "@babel/helper-validator-identifier": "^7.25.9", "@babel/traverse": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw=="], 38 | 39 | "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.25.9", "", {}, "sha512-kSMlyUVdWe25rEsRGviIgOWnoT/nfABVWlqt9N19/dIPWViAOW2s9wznP5tURbs/IDuNk4gPy3YdYRgH3uxhBw=="], 40 | 41 | "@babel/helper-string-parser": ["@babel/helper-string-parser@7.25.9", "", {}, "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA=="], 42 | 43 | "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.25.9", "", {}, "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ=="], 44 | 45 | "@babel/helper-validator-option": ["@babel/helper-validator-option@7.25.9", "", {}, "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw=="], 46 | 47 | "@babel/helpers": ["@babel/helpers@7.26.0", "", { "dependencies": { "@babel/template": "^7.25.9", "@babel/types": "^7.26.0" } }, "sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw=="], 48 | 49 | "@babel/parser": ["@babel/parser@7.26.2", "", { "dependencies": { "@babel/types": "^7.26.0" }, "bin": "./bin/babel-parser.js" }, "sha512-DWMCZH9WA4Maitz2q21SRKHo9QXZxkDsbNZoVD62gusNtNBBqDg9i7uOhASfTfIGNzW+O+r7+jAlM8dwphcJKQ=="], 50 | 51 | "@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-y8quW6p0WHkEhmErnfe58r7x0A70uKphQm8Sp8cV7tjNQwK56sNVK0M73LK3WuYmsuyrftut4xAkjjgU0twaMg=="], 52 | 53 | "@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-+iqjT8xmXhhYv4/uiYd8FNQsraMFZIfxVSqxxVSZP0WbbSAWvBXAul0m/zu+7Vv4O/3WtApy9pmaTMiumEZgfg=="], 54 | 55 | "@babel/template": ["@babel/template@7.25.9", "", { "dependencies": { "@babel/code-frame": "^7.25.9", "@babel/parser": "^7.25.9", "@babel/types": "^7.25.9" } }, "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg=="], 56 | 57 | "@babel/traverse": ["@babel/traverse@7.25.9", "", { "dependencies": { "@babel/code-frame": "^7.25.9", "@babel/generator": "^7.25.9", "@babel/parser": "^7.25.9", "@babel/template": "^7.25.9", "@babel/types": "^7.25.9", "debug": "^4.3.1", "globals": "^11.1.0" } }, "sha512-ZCuvfwOwlz/bawvAuvcj8rrithP2/N55Tzz342AkTvq4qaWbGfmCk/tKhNaV2cthijKrPAA8SRJV5WWe7IBMJw=="], 58 | 59 | "@babel/types": ["@babel/types@7.26.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.25.9", "@babel/helper-validator-identifier": "^7.25.9" } }, "sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA=="], 60 | 61 | "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.24.0", "", { "os": "aix", "cpu": "ppc64" }, "sha512-WtKdFM7ls47zkKHFVzMz8opM7LkcsIp9amDUBIAWirg70RM71WRSjdILPsY5Uv1D42ZpUfaPILDlfactHgsRkw=="], 62 | 63 | "@esbuild/android-arm": ["@esbuild/android-arm@0.24.0", "", { "os": "android", "cpu": "arm" }, "sha512-arAtTPo76fJ/ICkXWetLCc9EwEHKaeya4vMrReVlEIUCAUncH7M4bhMQ+M9Vf+FFOZJdTNMXNBrWwW+OXWpSew=="], 64 | 65 | "@esbuild/android-arm64": ["@esbuild/android-arm64@0.24.0", "", { "os": "android", "cpu": "arm64" }, "sha512-Vsm497xFM7tTIPYK9bNTYJyF/lsP590Qc1WxJdlB6ljCbdZKU9SY8i7+Iin4kyhV/KV5J2rOKsBQbB77Ab7L/w=="], 66 | 67 | "@esbuild/android-x64": ["@esbuild/android-x64@0.24.0", "", { "os": "android", "cpu": "x64" }, "sha512-t8GrvnFkiIY7pa7mMgJd7p8p8qqYIz1NYiAoKc75Zyv73L3DZW++oYMSHPRarcotTKuSs6m3hTOa5CKHaS02TQ=="], 68 | 69 | "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.24.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-CKyDpRbK1hXwv79soeTJNHb5EiG6ct3efd/FTPdzOWdbZZfGhpbcqIpiD0+vwmpu0wTIL97ZRPZu8vUt46nBSw=="], 70 | 71 | "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.24.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-rgtz6flkVkh58od4PwTRqxbKH9cOjaXCMZgWD905JOzjFKW+7EiUObfd/Kav+A6Gyud6WZk9w+xu6QLytdi2OA=="], 72 | 73 | "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.24.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-6Mtdq5nHggwfDNLAHkPlyLBpE5L6hwsuXZX8XNmHno9JuL2+bg2BX5tRkwjyfn6sKbxZTq68suOjgWqCicvPXA=="], 74 | 75 | "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.24.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-D3H+xh3/zphoX8ck4S2RxKR6gHlHDXXzOf6f/9dbFt/NRBDIE33+cVa49Kil4WUjxMGW0ZIYBYtaGCa2+OsQwQ=="], 76 | 77 | "@esbuild/linux-arm": ["@esbuild/linux-arm@0.24.0", "", { "os": "linux", "cpu": "arm" }, "sha512-gJKIi2IjRo5G6Glxb8d3DzYXlxdEj2NlkixPsqePSZMhLudqPhtZ4BUrpIuTjJYXxvF9njql+vRjB2oaC9XpBw=="], 78 | 79 | "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.24.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-TDijPXTOeE3eaMkRYpcy3LarIg13dS9wWHRdwYRnzlwlA370rNdZqbcp0WTyyV/k2zSxfko52+C7jU5F9Tfj1g=="], 80 | 81 | "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.24.0", "", { "os": "linux", "cpu": "ia32" }, "sha512-K40ip1LAcA0byL05TbCQ4yJ4swvnbzHscRmUilrmP9Am7//0UjPreh4lpYzvThT2Quw66MhjG//20mrufm40mA=="], 82 | 83 | "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.24.0", "", { "os": "linux", "cpu": "none" }, "sha512-0mswrYP/9ai+CU0BzBfPMZ8RVm3RGAN/lmOMgW4aFUSOQBjA31UP8Mr6DDhWSuMwj7jaWOT0p0WoZ6jeHhrD7g=="], 84 | 85 | "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.24.0", "", { "os": "linux", "cpu": "none" }, "sha512-hIKvXm0/3w/5+RDtCJeXqMZGkI2s4oMUGj3/jM0QzhgIASWrGO5/RlzAzm5nNh/awHE0A19h/CvHQe6FaBNrRA=="], 86 | 87 | "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.24.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-HcZh5BNq0aC52UoocJxaKORfFODWXZxtBaaZNuN3PUX3MoDsChsZqopzi5UupRhPHSEHotoiptqikjN/B77mYQ=="], 88 | 89 | "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.24.0", "", { "os": "linux", "cpu": "none" }, "sha512-bEh7dMn/h3QxeR2KTy1DUszQjUrIHPZKyO6aN1X4BCnhfYhuQqedHaa5MxSQA/06j3GpiIlFGSsy1c7Gf9padw=="], 90 | 91 | "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.24.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-ZcQ6+qRkw1UcZGPyrCiHHkmBaj9SiCD8Oqd556HldP+QlpUIe2Wgn3ehQGVoPOvZvtHm8HPx+bH20c9pvbkX3g=="], 92 | 93 | "@esbuild/linux-x64": ["@esbuild/linux-x64@0.24.0", "", { "os": "linux", "cpu": "x64" }, "sha512-vbutsFqQ+foy3wSSbmjBXXIJ6PL3scghJoM8zCL142cGaZKAdCZHyf+Bpu/MmX9zT9Q0zFBVKb36Ma5Fzfa8xA=="], 94 | 95 | "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.24.0", "", { "os": "none", "cpu": "x64" }, "sha512-hjQ0R/ulkO8fCYFsG0FZoH+pWgTTDreqpqY7UnQntnaKv95uP5iW3+dChxnx7C3trQQU40S+OgWhUVwCjVFLvg=="], 96 | 97 | "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.24.0", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-MD9uzzkPQbYehwcN583yx3Tu5M8EIoTD+tUgKF982WYL9Pf5rKy9ltgD0eUgs8pvKnmizxjXZyLt0z6DC3rRXg=="], 98 | 99 | "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.24.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-4ir0aY1NGUhIC1hdoCzr1+5b43mw99uNwVzhIq1OY3QcEwPDO3B7WNXBzaKY5Nsf1+N11i1eOfFcq+D/gOS15Q=="], 100 | 101 | "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.24.0", "", { "os": "sunos", "cpu": "x64" }, "sha512-jVzdzsbM5xrotH+W5f1s+JtUy1UWgjU0Cf4wMvffTB8m6wP5/kx0KiaLHlbJO+dMgtxKV8RQ/JvtlFcdZ1zCPA=="], 102 | 103 | "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.24.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-iKc8GAslzRpBytO2/aN3d2yb2z8XTVfNV0PjGlCxKo5SgWmNXx82I/Q3aG1tFfS+A2igVCY97TJ8tnYwpUWLCA=="], 104 | 105 | "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.24.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-vQW36KZolfIudCcTnaTpmLQ24Ha1RjygBo39/aLkM2kmjkWmZGEJ5Gn9l5/7tzXA42QGIoWbICfg6KLLkIw6yw=="], 106 | 107 | "@esbuild/win32-x64": ["@esbuild/win32-x64@0.24.0", "", { "os": "win32", "cpu": "x64" }, "sha512-7IAFPrjSQIJrGsK6flwg7NFmwBoSTyF3rl7If0hNUFQU4ilTsEPL6GuMuU9BfIWVVGuRnuIidkSMC+c0Otu8IA=="], 108 | 109 | "@fable-org/fable-library-js": ["@fable-org/fable-library-js@2.0.0-beta.3", "", {}, "sha512-gUXVwkAYMM4vbOk1iySv4smE1SzYaXJQ4hieqNVmN/6ojpafjprLt5gupHdlkwcMZriXX+F32Rzn8oNfJicQ5Q=="], 110 | 111 | "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.5", "", { "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg=="], 112 | 113 | "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], 114 | 115 | "@jridgewell/set-array": ["@jridgewell/set-array@1.2.1", "", {}, "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A=="], 116 | 117 | "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="], 118 | 119 | "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="], 120 | 121 | "@polka/url": ["@polka/url@1.0.0-next.28", "", {}, "sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw=="], 122 | 123 | "@rollup/pluginutils": ["@rollup/pluginutils@5.1.3", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-Pnsb6f32CD2W3uCaLZIzDmeFyQ2b8UWMFI7xtwUezpcGBDVDW6y9XgAWIlARiGAo6eNF5FK5aQTr0LFyNyqq5A=="], 124 | 125 | "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.27.4", "", { "os": "android", "cpu": "arm" }, "sha512-2Y3JT6f5MrQkICUyRVCw4oa0sutfAsgaSsb0Lmmy1Wi2y7X5vT9Euqw4gOsCyy0YfKURBg35nhUKZS4mDcfULw=="], 126 | 127 | "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.27.4", "", { "os": "android", "cpu": "arm64" }, "sha512-wzKRQXISyi9UdCVRqEd0H4cMpzvHYt1f/C3CoIjES6cG++RHKhrBj2+29nPF0IB5kpy9MS71vs07fvrNGAl/iA=="], 128 | 129 | "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.27.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-PlNiRQapift4LNS8DPUHuDX/IdXiLjf8mc5vdEmUR0fF/pyy2qWwzdLjB+iZquGr8LuN4LnUoSEvKRwjSVYz3Q=="], 130 | 131 | "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.27.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-o9bH2dbdgBDJaXWJCDTNDYa171ACUdzpxSZt+u/AAeQ20Nk5x+IhA+zsGmrQtpkLiumRJEYef68gcpn2ooXhSQ=="], 132 | 133 | "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.27.4", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-NBI2/i2hT9Q+HySSHTBh52da7isru4aAAo6qC3I7QFVsuhxi2gM8t/EI9EVcILiHLj1vfi+VGGPaLOUENn7pmw=="], 134 | 135 | "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.27.4", "", { "os": "freebsd", "cpu": "x64" }, "sha512-wYcC5ycW2zvqtDYrE7deary2P2UFmSh85PUpAx+dwTCO9uw3sgzD6Gv9n5X4vLaQKsrfTSZZ7Z7uynQozPVvWA=="], 136 | 137 | "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.27.4", "", { "os": "linux", "cpu": "arm" }, "sha512-9OwUnK/xKw6DyRlgx8UizeqRFOfi9mf5TYCw1uolDaJSbUmBxP85DE6T4ouCMoN6pXw8ZoTeZCSEfSaYo+/s1w=="], 138 | 139 | "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.27.4", "", { "os": "linux", "cpu": "arm" }, "sha512-Vgdo4fpuphS9V24WOV+KwkCVJ72u7idTgQaBoLRD0UxBAWTF9GWurJO9YD9yh00BzbkhpeXtm6na+MvJU7Z73A=="], 140 | 141 | "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.27.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-pleyNgyd1kkBkw2kOqlBx+0atfIIkkExOTiifoODo6qKDSpnc6WzUY5RhHdmTdIJXBdSnh6JknnYTtmQyobrVg=="], 142 | 143 | "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.27.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-caluiUXvUuVyCHr5DxL8ohaaFFzPGmgmMvwmqAITMpV/Q+tPoaHZ/PWa3t8B2WyoRcIIuu1hkaW5KkeTDNSnMA=="], 144 | 145 | "@rollup/rollup-linux-powerpc64le-gnu": ["@rollup/rollup-linux-powerpc64le-gnu@4.27.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-FScrpHrO60hARyHh7s1zHE97u0KlT/RECzCKAdmI+LEoC1eDh/RDji9JgFqyO+wPDb86Oa/sXkily1+oi4FzJQ=="], 146 | 147 | "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.27.4", "", { "os": "linux", "cpu": "none" }, "sha512-qyyprhyGb7+RBfMPeww9FlHwKkCXdKHeGgSqmIXw9VSUtvyFZ6WZRtnxgbuz76FK7LyoN8t/eINRbPUcvXB5fw=="], 148 | 149 | "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.27.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-PFz+y2kb6tbh7m3A7nA9++eInGcDVZUACulf/KzDtovvdTizHpZaJty7Gp0lFwSQcrnebHOqxF1MaKZd7psVRg=="], 150 | 151 | "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.27.4", "", { "os": "linux", "cpu": "x64" }, "sha512-Ni8mMtfo+o/G7DVtweXXV/Ol2TFf63KYjTtoZ5f078AUgJTmaIJnj4JFU7TK/9SVWTaSJGxPi5zMDgK4w+Ez7Q=="], 152 | 153 | "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.27.4", "", { "os": "linux", "cpu": "x64" }, "sha512-5AeeAF1PB9TUzD+3cROzFTnAJAcVUGLuR8ng0E0WXGkYhp6RD6L+6szYVX+64Rs0r72019KHZS1ka1q+zU/wUw=="], 154 | 155 | "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.27.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-yOpVsA4K5qVwu2CaS3hHxluWIK5HQTjNV4tWjQXluMiiiu4pJj4BN98CvxohNCpcjMeTXk/ZMJBRbgRg8HBB6A=="], 156 | 157 | "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.27.4", "", { "os": "win32", "cpu": "ia32" }, "sha512-KtwEJOaHAVJlxV92rNYiG9JQwQAdhBlrjNRp7P9L8Cb4Rer3in+0A+IPhJC9y68WAi9H0sX4AiG2NTsVlmqJeQ=="], 158 | 159 | "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.27.4", "", { "os": "win32", "cpu": "x64" }, "sha512-3j4jx1TppORdTAoBJRd+/wJRGCPC0ETWkXOecJ6PPZLj6SptXkrXcNqdj0oclbKML6FkQltdz7bBA3rUSirZug=="], 160 | 161 | "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="], 162 | 163 | "@types/babel__generator": ["@types/babel__generator@7.6.8", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw=="], 164 | 165 | "@types/babel__template": ["@types/babel__template@7.4.4", "", { "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" } }, "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A=="], 166 | 167 | "@types/babel__traverse": ["@types/babel__traverse@7.20.6", "", { "dependencies": { "@babel/types": "^7.20.7" } }, "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg=="], 168 | 169 | "@types/estree": ["@types/estree@1.0.6", "", {}, "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw=="], 170 | 171 | "@types/prop-types": ["@types/prop-types@15.7.13", "", {}, "sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA=="], 172 | 173 | "@types/react": ["@types/react@18.3.12", "", { "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" } }, "sha512-D2wOSq/d6Agt28q7rSI3jhU7G6aiuzljDGZ2hTZHIkrTLUI+AF3WMeKkEZ9nN2fkBAlcktT6vcZjDFiIhMYEQw=="], 174 | 175 | "@types/react-dom": ["@types/react-dom@18.3.1", "", { "dependencies": { "@types/react": "*" } }, "sha512-qW1Mfv8taImTthu4KoXgDfLuk4bydU6Q/TkADnDWWHwi4NX4BR+LWfTp2sVmTqRrsHvyDDTelgelxJ+SsejKKQ=="], 176 | 177 | "@vitejs/plugin-react": ["@vitejs/plugin-react@4.3.4", "", { "dependencies": { "@babel/core": "^7.26.0", "@babel/plugin-transform-react-jsx-self": "^7.25.9", "@babel/plugin-transform-react-jsx-source": "^7.25.9", "@types/babel__core": "^7.20.5", "react-refresh": "^0.14.2" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0" } }, "sha512-SCCPBJtYLdE8PX/7ZQAs1QAZ8Jqwih+0VBLum1EGqmCCQal+MIUqLCzj3ZUy8ufbC0cAM4LRlSTm7IQJwWT4ug=="], 178 | 179 | "browserslist": ["browserslist@4.24.2", "", { "dependencies": { "caniuse-lite": "^1.0.30001669", "electron-to-chromium": "^1.5.41", "node-releases": "^2.0.18", "update-browserslist-db": "^1.1.1" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg=="], 180 | 181 | "bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="], 182 | 183 | "caniuse-lite": ["caniuse-lite@1.0.30001684", "", {}, "sha512-G1LRwLIQjBQoyq0ZJGqGIJUXzJ8irpbjHLpVRXDvBEScFJ9b17sgK6vlx0GAJFE21okD7zXl08rRRUfq6HdoEQ=="], 184 | 185 | "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], 186 | 187 | "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], 188 | 189 | "debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="], 190 | 191 | "default-browser": ["default-browser@5.2.1", "", { "dependencies": { "bundle-name": "^4.1.0", "default-browser-id": "^5.0.0" } }, "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg=="], 192 | 193 | "default-browser-id": ["default-browser-id@5.0.0", "", {}, "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA=="], 194 | 195 | "define-lazy-prop": ["define-lazy-prop@3.0.0", "", {}, "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg=="], 196 | 197 | "electron-to-chromium": ["electron-to-chromium@1.5.65", "", {}, "sha512-PWVzBjghx7/wop6n22vS2MLU8tKGd4Q91aCEGhG/TYmW6PP5OcSXcdnxTe1NNt0T66N8D6jxh4kC8UsdzOGaIw=="], 198 | 199 | "error-stack-parser-es": ["error-stack-parser-es@0.1.5", "", {}, "sha512-xHku1X40RO+fO8yJ8Wh2f2rZWVjqyhb1zgq1yZ8aZRQkv6OOKhKWRUaht3eSCUbAOBaKIgM+ykwFLE+QUxgGeg=="], 200 | 201 | "esbuild": ["esbuild@0.24.0", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.24.0", "@esbuild/android-arm": "0.24.0", "@esbuild/android-arm64": "0.24.0", "@esbuild/android-x64": "0.24.0", "@esbuild/darwin-arm64": "0.24.0", "@esbuild/darwin-x64": "0.24.0", "@esbuild/freebsd-arm64": "0.24.0", "@esbuild/freebsd-x64": "0.24.0", "@esbuild/linux-arm": "0.24.0", "@esbuild/linux-arm64": "0.24.0", "@esbuild/linux-ia32": "0.24.0", "@esbuild/linux-loong64": "0.24.0", "@esbuild/linux-mips64el": "0.24.0", "@esbuild/linux-ppc64": "0.24.0", "@esbuild/linux-riscv64": "0.24.0", "@esbuild/linux-s390x": "0.24.0", "@esbuild/linux-x64": "0.24.0", "@esbuild/netbsd-x64": "0.24.0", "@esbuild/openbsd-arm64": "0.24.0", "@esbuild/openbsd-x64": "0.24.0", "@esbuild/sunos-x64": "0.24.0", "@esbuild/win32-arm64": "0.24.0", "@esbuild/win32-ia32": "0.24.0", "@esbuild/win32-x64": "0.24.0" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-FuLPevChGDshgSicjisSooU0cemp/sGXR841D5LHMB7mTVOmsEHcAxaH3irL53+8YDIeVNQEySh4DaYU/iuPqQ=="], 202 | 203 | "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], 204 | 205 | "estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], 206 | 207 | "fs-extra": ["fs-extra@11.2.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw=="], 208 | 209 | "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], 210 | 211 | "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], 212 | 213 | "globals": ["globals@11.12.0", "", {}, "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="], 214 | 215 | "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], 216 | 217 | "is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="], 218 | 219 | "is-inside-container": ["is-inside-container@1.0.0", "", { "dependencies": { "is-docker": "^3.0.0" }, "bin": { "is-inside-container": "cli.js" } }, "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA=="], 220 | 221 | "is-wsl": ["is-wsl@3.1.0", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw=="], 222 | 223 | "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], 224 | 225 | "jsesc": ["jsesc@3.0.2", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g=="], 226 | 227 | "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], 228 | 229 | "jsonfile": ["jsonfile@6.1.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ=="], 230 | 231 | "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="], 232 | 233 | "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], 234 | 235 | "mrmime": ["mrmime@2.0.0", "", {}, "sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw=="], 236 | 237 | "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], 238 | 239 | "nanoid": ["nanoid@3.3.8", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w=="], 240 | 241 | "node-releases": ["node-releases@2.0.18", "", {}, "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g=="], 242 | 243 | "open": ["open@10.1.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "is-wsl": "^3.1.0" } }, "sha512-mnkeQ1qP5Ue2wd+aivTD3NHd/lZ96Lu0jgf0pwktLPtx6cTZiH7tyeGRRHs0zX0rbrahXPnXlUnbeXyaBBuIaw=="], 244 | 245 | "perfect-debounce": ["perfect-debounce@1.0.0", "", {}, "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA=="], 246 | 247 | "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], 248 | 249 | "picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="], 250 | 251 | "postcss": ["postcss@8.4.49", "", { "dependencies": { "nanoid": "^3.3.7", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA=="], 252 | 253 | "react": ["react@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ=="], 254 | 255 | "react-dom": ["react-dom@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" }, "peerDependencies": { "react": "^18.3.1" } }, "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw=="], 256 | 257 | "react-refresh": ["react-refresh@0.14.2", "", {}, "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA=="], 258 | 259 | "rollup": ["rollup@4.27.4", "", { "dependencies": { "@types/estree": "1.0.6" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.27.4", "@rollup/rollup-android-arm64": "4.27.4", "@rollup/rollup-darwin-arm64": "4.27.4", "@rollup/rollup-darwin-x64": "4.27.4", "@rollup/rollup-freebsd-arm64": "4.27.4", "@rollup/rollup-freebsd-x64": "4.27.4", "@rollup/rollup-linux-arm-gnueabihf": "4.27.4", "@rollup/rollup-linux-arm-musleabihf": "4.27.4", "@rollup/rollup-linux-arm64-gnu": "4.27.4", "@rollup/rollup-linux-arm64-musl": "4.27.4", "@rollup/rollup-linux-powerpc64le-gnu": "4.27.4", "@rollup/rollup-linux-riscv64-gnu": "4.27.4", "@rollup/rollup-linux-s390x-gnu": "4.27.4", "@rollup/rollup-linux-x64-gnu": "4.27.4", "@rollup/rollup-linux-x64-musl": "4.27.4", "@rollup/rollup-win32-arm64-msvc": "4.27.4", "@rollup/rollup-win32-ia32-msvc": "4.27.4", "@rollup/rollup-win32-x64-msvc": "4.27.4", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-RLKxqHEMjh/RGLsDxAEsaLO3mWgyoU6x9w6n1ikAzet4B3gI2/3yP6PWY2p9QzRTh6MfEIXB3MwsOY0Iv3vNrw=="], 260 | 261 | "run-applescript": ["run-applescript@7.0.0", "", {}, "sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A=="], 262 | 263 | "scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="], 264 | 265 | "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], 266 | 267 | "sirv": ["sirv@3.0.0", "", { "dependencies": { "@polka/url": "^1.0.0-next.24", "mrmime": "^2.0.0", "totalist": "^3.0.0" } }, "sha512-BPwJGUeDaDCHihkORDchNyyTvWFhcusy1XMmhEVTQTwGeybFbp8YEmB+njbPnth1FibULBSBVwCQni25XlCUDg=="], 268 | 269 | "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], 270 | 271 | "totalist": ["totalist@3.0.1", "", {}, "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ=="], 272 | 273 | "universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], 274 | 275 | "update-browserslist-db": ["update-browserslist-db@1.1.1", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.0" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A=="], 276 | 277 | "vite": ["vite@6.0.1", "", { "dependencies": { "esbuild": "^0.24.0", "postcss": "^8.4.49", "rollup": "^4.23.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-Ldn6gorLGr4mCdFnmeAOLweJxZ34HjKnDm4HGo6P66IEqTxQb36VEdFJQENKxWjupNfoIjvRUnswjn1hpYEpjQ=="], 278 | 279 | "vite-plugin-inspect": ["vite-plugin-inspect@0.8.8", "", { "dependencies": { "@antfu/utils": "^0.7.10", "@rollup/pluginutils": "^5.1.3", "debug": "^4.3.7", "error-stack-parser-es": "^0.1.5", "fs-extra": "^11.2.0", "open": "^10.1.0", "perfect-debounce": "^1.0.0", "picocolors": "^1.1.1", "sirv": "^3.0.0" }, "peerDependencies": { "vite": "^3.1.0 || ^4.0.0 || ^5.0.0-0" } }, "sha512-aZlBuXsWUPJFmMK92GIv6lH7LrwG2POu4KJ+aEdcqnu92OAf+rhBnfMDQvxIJPEB7hE2t5EyY/PMgf5aDLT8EA=="], 280 | 281 | "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], 282 | } 283 | } 284 | -------------------------------------------------------------------------------- /sample-project/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React 8 | 9 | 10 |
    11 |

    Static text

    12 |
    13 |
    14 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /sample-project/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sample-project", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "bunx --bun vite", 8 | "build": "bunx --bun vite build", 9 | "preview": "bunx --bun vite preview" 10 | }, 11 | "dependencies": { 12 | "@fable-org/fable-library-js": "2.0.0-beta.3", 13 | "react": "^18.3.1", 14 | "react-dom": "^18.3.1" 15 | }, 16 | "devDependencies": { 17 | "@types/react": "^18.3.12", 18 | "@types/react-dom": "^18.3.1", 19 | "@vitejs/plugin-react": "4.3.4", 20 | "vite": "^6.0.1", 21 | "vite-plugin-inspect": "0.8.8" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /sample-project/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /sample-project/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import Inspect from "vite-plugin-inspect"; 3 | import fable from "../index.js"; 4 | import react from "@vitejs/plugin-react"; 5 | 6 | // https://vitejs.dev/config/ 7 | export default defineConfig({ 8 | server: { 9 | port: 4000, 10 | }, 11 | plugins: [ 12 | Inspect(), 13 | fable({ jsx: "automatic" }), 14 | react({ include: /\.fs$/ }), 15 | ], 16 | }); 17 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "checkJs": true, 5 | "noEmit": true, 6 | "strict": false, 7 | "target": "ESNext", 8 | "module": "NodeNext", 9 | "jsx": "react" 10 | }, 11 | "include": ["./index.js", "./docs/scripts/command.js"] 12 | } 13 | -------------------------------------------------------------------------------- /types.d.ts: -------------------------------------------------------------------------------- 1 | import { Logger } from "vite"; 2 | import { ChildProcessWithoutNullStreams } from "node:child_process"; 3 | import { Subscription } from "rxjs"; 4 | import { JSONRPCEndpoint } from "ts-lsp-client"; 5 | 6 | // Represents a generic F# discriminated union case with associated fields. 7 | export interface FSharpDiscriminatedUnion { 8 | case: string; // The name of the case (mirroring the F# case name) 9 | fields: any[]; // The fields associated with the case 10 | } 11 | 12 | // Represents options for an F# project. 13 | export interface FSharpProjectOptions { 14 | sourceFiles: string[]; // List of source files in the project 15 | } 16 | 17 | // Defines a range within a file, used for diagnostics or annotations. 18 | export interface DiagnosticRange { 19 | startLine: number; // The start line of the diagnostic range 20 | startColumn: number; // The start column of the diagnostic range 21 | endLine: number; // The end line of the diagnostic range 22 | endColumn: number; // The end column of the diagnostic range 23 | } 24 | 25 | // Describes a diagnostic message, typically an error or warning, within a file. 26 | export interface Diagnostic { 27 | errorNumberText: string; // The error number or identifier text 28 | message: string; // The descriptive diagnostic message 29 | range: DiagnosticRange; // The range within the file where the diagnostic applies 30 | severity: string; // The severity of the diagnostic (e.g., error, warning) 31 | fileName: string; // The name of the file containing the diagnostic 32 | } 33 | 34 | export interface PluginOptions { 35 | fsproj?: string; // Optional: The main fsproj to load 36 | jsx?: "transform" | "preserve" | "automatic" | null; // Optional: Apply JSX transformation after Fable compilation 37 | noReflection?: boolean; // Optional: Pass noReflection value to Fable.Compiler 38 | exclude?: string[]; // Optional: Pass exclude to Fable.Compiler 39 | } 40 | 41 | export interface PluginState { 42 | config: PluginOptions; 43 | logger: Logger; 44 | dotnetProcess: ChildProcessWithoutNullStreams | null; 45 | endpoint: JSONRPCEndpoint | null; 46 | compilableFiles: Map; 47 | sourceFiles: Set; 48 | fsproj: string | null; 49 | configuration: string; 50 | dependentFiles: Set; 51 | pendingChanges: Subscription | null; 52 | hotPromiseWithResolvers: PromiseWithResolvers>; 53 | isBuild: boolean; 54 | } 55 | 56 | // Represents an event where an F# file has changed. 57 | export interface FSharpFileChanged { 58 | type: "FSharpFileChanged"; // Discriminator for FSharpFileChanged event type 59 | file: string; // The F# file that changed 60 | } 61 | 62 | // Represents an event where a project file has changed. 63 | export interface ProjectFileChanged { 64 | type: "ProjectFileChanged"; // Discriminator for ProjectFileChanged event type 65 | file: string; // The project file that changed 66 | } 67 | 68 | // Type that represents the possible hook events. Acts as a discriminated union in TypeScript. 69 | export type HookEvent = FSharpFileChanged | ProjectFileChanged; 70 | 71 | // Represents the state of pending changes. 72 | export interface PendingChangesState { 73 | projectChanged: boolean; // Indicates whether the project changed 74 | fsharpFiles: Set; // Set of changed F# files 75 | projectFiles: Set; // Set of changed project files 76 | } 77 | 78 | export interface ProjectFileData { 79 | sourceFiles: string[]; 80 | diagnostics: Diagnostic[]; 81 | dependentFiles: string[]; 82 | } 83 | -------------------------------------------------------------------------------- /vite-plugin-fable.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.0.31903.59 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Fable.Daemon", "Fable.Daemon\Fable.Daemon.fsproj", "{0F2A7F4B-1FB2-4752-9598-C95B4BA74982}" 7 | EndProject 8 | Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "App", "sample-project\App.fsproj", "{511D6D79-DCD1-4596-A94E-DD5E19C8C3E1}" 9 | EndProject 10 | Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Fable.Daemon.Tests", "Fable.Daemon.Tests\Fable.Daemon.Tests.fsproj", "{C0D489F9-27C5-49D6-B674-209C30694BEC}" 11 | EndProject 12 | Global 13 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 14 | Debug|Any CPU = Debug|Any CPU 15 | Release|Any CPU = Release|Any CPU 16 | EndGlobalSection 17 | GlobalSection(SolutionProperties) = preSolution 18 | HideSolutionNode = FALSE 19 | EndGlobalSection 20 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 21 | {0F2A7F4B-1FB2-4752-9598-C95B4BA74982}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 22 | {0F2A7F4B-1FB2-4752-9598-C95B4BA74982}.Debug|Any CPU.Build.0 = Debug|Any CPU 23 | {0F2A7F4B-1FB2-4752-9598-C95B4BA74982}.Release|Any CPU.ActiveCfg = Release|Any CPU 24 | {0F2A7F4B-1FB2-4752-9598-C95B4BA74982}.Release|Any CPU.Build.0 = Release|Any CPU 25 | {511D6D79-DCD1-4596-A94E-DD5E19C8C3E1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 26 | {511D6D79-DCD1-4596-A94E-DD5E19C8C3E1}.Debug|Any CPU.Build.0 = Debug|Any CPU 27 | {511D6D79-DCD1-4596-A94E-DD5E19C8C3E1}.Release|Any CPU.ActiveCfg = Release|Any CPU 28 | {511D6D79-DCD1-4596-A94E-DD5E19C8C3E1}.Release|Any CPU.Build.0 = Release|Any CPU 29 | {C0D489F9-27C5-49D6-B674-209C30694BEC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 30 | {C0D489F9-27C5-49D6-B674-209C30694BEC}.Debug|Any CPU.Build.0 = Debug|Any CPU 31 | {C0D489F9-27C5-49D6-B674-209C30694BEC}.Release|Any CPU.ActiveCfg = Release|Any CPU 32 | {C0D489F9-27C5-49D6-B674-209C30694BEC}.Release|Any CPU.Build.0 = Release|Any CPU 33 | EndGlobalSection 34 | EndGlobal 35 | --------------------------------------------------------------------------------