Repository: denoland/fresh Branch: main Commit: 4cc76aefed73 Files: 485 Total size: 1.3 MB Directory structure: gitextract__i6wh6to/ ├── .gitattributes ├── .github/ │ ├── CODE_OF_CONDUCT.md │ ├── CONTRIBUTING.md │ ├── dependabot.yml │ └── workflows/ │ ├── ci.yml │ ├── deploy.yml │ ├── post_publish.yml │ └── publish.yml ├── .gitignore ├── .vscode/ │ ├── extensions.json │ ├── settings.json │ └── tailwind.json ├── LICENSE ├── README.md ├── _typos.toml ├── deno.json ├── docs/ │ ├── 1.x/ │ │ ├── concepts/ │ │ │ ├── ahead-of-time-builds.md │ │ │ ├── app-wrapper.md │ │ │ ├── architecture.md │ │ │ ├── data-fetching.md │ │ │ ├── deployment.md │ │ │ ├── error-pages.md │ │ │ ├── forms.md │ │ │ ├── index.md │ │ │ ├── islands.md │ │ │ ├── layouts.md │ │ │ ├── middleware.md │ │ │ ├── partials.md │ │ │ ├── plugins.md │ │ │ ├── routes.md │ │ │ ├── routing.md │ │ │ ├── server-components.md │ │ │ ├── server-configuration.md │ │ │ ├── static-files.md │ │ │ └── updating.md │ │ ├── examples/ │ │ │ ├── active-links.md │ │ │ ├── authentication-with-supabase.md │ │ │ ├── changing-the-src-dir.md │ │ │ ├── client-side-components-and-libraries.md │ │ │ ├── creating-a-crud-api.md │ │ │ ├── dealing-with-cors.md │ │ │ ├── handling-complex-routes.md │ │ │ ├── index.md │ │ │ ├── init-the-server.md │ │ │ ├── migrating-to-tailwind.md │ │ │ ├── modifying-the-head.md │ │ │ ├── rendering-markdown.md │ │ │ ├── rendering-raw-html.md │ │ │ ├── setting-the-language.md │ │ │ ├── sharing-state-between-islands.md │ │ │ ├── using-csp.md │ │ │ ├── using-fresh-canary-version.md │ │ │ ├── using-twind-v1.md │ │ │ └── writing-tests.md │ │ ├── getting-started/ │ │ │ ├── adding-interactivity.md │ │ │ ├── create-a-project.md │ │ │ ├── create-a-route.md │ │ │ ├── custom-handlers.md │ │ │ ├── deploy-to-production.md │ │ │ ├── dynamic-routes.md │ │ │ ├── form-submissions.md │ │ │ ├── index.md │ │ │ └── running-locally.md │ │ ├── integrations/ │ │ │ └── index.md │ │ └── introduction/ │ │ └── index.md │ ├── canary/ │ │ └── the-canary-version/ │ │ └── index.md │ ├── latest/ │ │ ├── advanced/ │ │ │ ├── app-wrapper.md │ │ │ ├── builder.md │ │ │ ├── define.md │ │ │ ├── environment-variables.md │ │ │ ├── error-handling.md │ │ │ ├── forms.md │ │ │ ├── head.md │ │ │ ├── index.md │ │ │ ├── layouts.md │ │ │ ├── partials.md │ │ │ ├── troubleshooting.md │ │ │ └── vite.md │ │ ├── concepts/ │ │ │ ├── app.md │ │ │ ├── context.md │ │ │ ├── file-routing.md │ │ │ ├── index.md │ │ │ ├── islands.md │ │ │ ├── layouts.md │ │ │ ├── middleware.md │ │ │ ├── routing.md │ │ │ └── static-files.md │ │ ├── deployment/ │ │ │ ├── cloudflare-workers.md │ │ │ ├── deno-compile.md │ │ │ ├── deno-deploy.md │ │ │ ├── docker.md │ │ │ └── index.md │ │ ├── examples/ │ │ │ ├── active-links.md │ │ │ ├── daisyui.md │ │ │ ├── markdown.md │ │ │ ├── migration-guide.md │ │ │ ├── rendering-raw-html.md │ │ │ └── sharing-state-between-islands.md │ │ ├── getting-started/ │ │ │ └── index.md │ │ ├── introduction/ │ │ │ └── index.md │ │ ├── plugins/ │ │ │ ├── cors.md │ │ │ ├── csp.md │ │ │ ├── csrf.md │ │ │ ├── index.md │ │ │ └── trailing-slashes.md │ │ └── testing/ │ │ └── index.md │ └── toc.ts ├── packages/ │ ├── build-id/ │ │ ├── README.md │ │ ├── deno.json │ │ └── mod.ts │ ├── examples/ │ │ ├── LICENSE │ │ ├── README.md │ │ ├── deno.json │ │ └── src/ │ │ ├── app1.tsx │ │ ├── app2.tsx │ │ ├── island.tsx │ │ └── shared.tsx │ ├── fresh/ │ │ ├── README.md │ │ ├── deno.json │ │ ├── src/ │ │ │ ├── app.ts │ │ │ ├── app_test.tsx │ │ │ ├── build_cache.ts │ │ │ ├── commands.ts │ │ │ ├── compat.ts │ │ │ ├── compat_test.tsx │ │ │ ├── config.ts │ │ │ ├── config_test.ts │ │ │ ├── constants.ts │ │ │ ├── context.ts │ │ │ ├── context_test.tsx │ │ │ ├── define.ts │ │ │ ├── define_test.ts │ │ │ ├── dev/ │ │ │ │ ├── builder.ts │ │ │ │ ├── builder_test.ts │ │ │ │ ├── check.ts │ │ │ │ ├── dev_build_cache.ts │ │ │ │ ├── dev_build_cache_test.ts │ │ │ │ ├── esbuild.ts │ │ │ │ ├── file_transformer.ts │ │ │ │ ├── file_transformer_test.ts │ │ │ │ ├── fs_crawl.ts │ │ │ │ ├── fs_crawl_test.ts │ │ │ │ ├── middlewares/ │ │ │ │ │ ├── automatic_workspace_folders.ts │ │ │ │ │ ├── error_overlay/ │ │ │ │ │ │ ├── code_frame.ts │ │ │ │ │ │ ├── middleware.tsx │ │ │ │ │ │ ├── middleware_test.tsx │ │ │ │ │ │ └── overlay.tsx │ │ │ │ │ └── live_reload.ts │ │ │ │ ├── mod.ts │ │ │ │ ├── update_check.ts │ │ │ │ └── update_check_test.ts │ │ │ ├── error.ts │ │ │ ├── error_test.ts │ │ │ ├── file_url.ts │ │ │ ├── file_url_test.ts │ │ │ ├── fs.ts │ │ │ ├── fs_routes.ts │ │ │ ├── fs_routes_test.tsx │ │ │ ├── handlers.ts │ │ │ ├── internals.ts │ │ │ ├── internals_dev.ts │ │ │ ├── jsonify/ │ │ │ │ ├── __snapshots__/ │ │ │ │ │ └── round_trip_test.ts.snap │ │ │ │ ├── constants.ts │ │ │ │ ├── custom_test.ts │ │ │ │ ├── parse.ts │ │ │ │ ├── round_trip_test.ts │ │ │ │ ├── stringify.ts │ │ │ │ └── stringify_test.ts │ │ │ ├── middlewares/ │ │ │ │ ├── cors.ts │ │ │ │ ├── cors_test.ts │ │ │ │ ├── csp.ts │ │ │ │ ├── csp_test.ts │ │ │ │ ├── csrf.ts │ │ │ │ ├── csrf_test.ts │ │ │ │ ├── mod.ts │ │ │ │ ├── mod_test.ts │ │ │ │ ├── static_files.ts │ │ │ │ ├── static_files_test.ts │ │ │ │ ├── trailing_slashes.ts │ │ │ │ └── trailing_slashes_test.ts │ │ │ ├── mod.ts │ │ │ ├── otel.ts │ │ │ ├── render.ts │ │ │ ├── router.ts │ │ │ ├── router_test.ts │ │ │ ├── runtime/ │ │ │ │ ├── client/ │ │ │ │ │ ├── dev.ts │ │ │ │ │ ├── mod.ts │ │ │ │ │ ├── partials.ts │ │ │ │ │ ├── polyfills.ts │ │ │ │ │ ├── preact_hooks_client.ts │ │ │ │ │ └── reviver.ts │ │ │ │ ├── head.ts │ │ │ │ ├── server/ │ │ │ │ │ └── preact_hooks.ts │ │ │ │ ├── shared.ts │ │ │ │ └── shared_internal.ts │ │ │ ├── segments.ts │ │ │ ├── segments_test.ts │ │ │ ├── server/ │ │ │ │ └── tailwind_aot_error_page.tsx │ │ │ ├── test_utils.ts │ │ │ ├── types.ts │ │ │ ├── utils.ts │ │ │ └── utils_test.ts │ │ └── tests/ │ │ ├── active_links_test.tsx │ │ ├── doc_examples_test.tsx │ │ ├── fixture_head/ │ │ │ ├── islands/ │ │ │ │ ├── MetaIsland.tsx │ │ │ │ ├── StyleIdIsland.tsx │ │ │ │ ├── TemplateIsland.tsx │ │ │ │ └── TitleIsland.tsx │ │ │ └── routes/ │ │ │ ├── _app.tsx │ │ │ ├── id.tsx │ │ │ ├── key.tsx │ │ │ ├── meta.tsx │ │ │ └── title.tsx │ │ ├── fixture_island_groups/ │ │ │ └── routes/ │ │ │ ├── both/ │ │ │ │ ├── (_islands)/ │ │ │ │ │ └── Foo.tsx │ │ │ │ └── index.tsx │ │ │ ├── foo/ │ │ │ │ ├── (_islands)/ │ │ │ │ │ └── Foo.tsx │ │ │ │ └── index.tsx │ │ │ └── index.tsx │ │ ├── fixture_precompile/ │ │ │ ├── invalid/ │ │ │ │ ├── deno.json │ │ │ │ ├── dev.ts │ │ │ │ └── main.tsx │ │ │ └── valid/ │ │ │ ├── deno.json │ │ │ └── main.tsx │ │ ├── fixture_update_check/ │ │ │ └── mod.ts │ │ ├── fixtures_islands/ │ │ │ ├── Computed.tsx │ │ │ ├── Counter.tsx │ │ │ ├── CounterWithSlots.tsx │ │ │ ├── EnvIsland.tsx │ │ │ ├── EscapeIsland.tsx │ │ │ ├── FnIsland.tsx │ │ │ ├── FragmentIsland.tsx │ │ │ ├── FreshAttrs.tsx │ │ │ ├── IslandInIsland.tsx │ │ │ ├── JsonIsland.tsx │ │ │ ├── JsxChildrenIsland.tsx │ │ │ ├── JsxConditional.tsx │ │ │ ├── JsxIsland.tsx │ │ │ ├── Multiple.tsx │ │ │ ├── NodeProcess.tsx │ │ │ ├── NullIsland.tsx │ │ │ ├── OptOutPartialLink.tsx │ │ │ ├── PartialInIsland.tsx │ │ │ ├── PassThrough.tsx │ │ │ ├── SelfCounter.tsx │ │ │ └── data.json │ │ ├── head_test.tsx │ │ ├── islands_test.tsx │ │ ├── lorem_ipsum.txt │ │ ├── partials_test.tsx │ │ ├── precompile_test.ts │ │ └── test_utils.tsx │ ├── init/ │ │ ├── README.md │ │ ├── deno.json │ │ └── src/ │ │ ├── init.ts │ │ ├── init_test.ts │ │ └── mod.ts │ ├── plugin-tailwindcss/ │ │ ├── README.md │ │ ├── deno.json │ │ └── src/ │ │ ├── mod.ts │ │ └── types.ts │ ├── plugin-tailwindcss-v3/ │ │ ├── README.md │ │ ├── deno.json │ │ └── src/ │ │ └── mod.ts │ ├── plugin-vite/ │ │ ├── README.md │ │ ├── demo/ │ │ │ ├── assets/ │ │ │ │ └── style.css │ │ │ ├── client.ts │ │ │ ├── components/ │ │ │ │ ├── CssModuleNonIsland.tsx │ │ │ │ └── CssModulesNonIsland.module.css │ │ │ ├── fixtures/ │ │ │ │ ├── commonjs_mod.cjs │ │ │ │ └── maxmind.cjs │ │ │ ├── islands/ │ │ │ │ ├── Bar.tsx │ │ │ │ ├── CssModules.module.css │ │ │ │ ├── CssModules.tsx │ │ │ │ ├── CssModulesOther.module.css │ │ │ │ ├── CssModulesOther.tsx │ │ │ │ ├── Foo.tsx │ │ │ │ ├── IslandNestedInner.tsx │ │ │ │ ├── IslandNestedOuter.tsx │ │ │ │ └── tests/ │ │ │ │ ├── CounterHooks.tsx │ │ │ │ ├── EnvIsland.tsx │ │ │ │ ├── IslandAssets.tsx │ │ │ │ ├── Mime.tsx │ │ │ │ └── Ready.tsx │ │ │ ├── main.ts │ │ │ ├── routes/ │ │ │ │ ├── _error.tsx │ │ │ │ ├── about.tsx │ │ │ │ ├── index.tsx │ │ │ │ └── tests/ │ │ │ │ ├── CssRoute.module.css │ │ │ │ ├── api/ │ │ │ │ │ └── [id].tsx │ │ │ │ ├── assets.tsx │ │ │ │ ├── build_id.tsx │ │ │ │ ├── commonjs.tsx │ │ │ │ ├── css.tsx │ │ │ │ ├── css_modules.tsx │ │ │ │ ├── css_styles.css │ │ │ │ ├── dep_json.tsx │ │ │ │ ├── env.tsx │ │ │ │ ├── env_files.tsx │ │ │ │ ├── feed.tsx │ │ │ │ ├── ioredis.tsx │ │ │ │ ├── island_assets.tsx │ │ │ │ ├── island_hooks.tsx │ │ │ │ ├── island_nested.tsx │ │ │ │ ├── it_works.tsx │ │ │ │ ├── jsx_namespace.tsx │ │ │ │ ├── maxmind.tsx │ │ │ │ ├── middlewares/ │ │ │ │ │ ├── _middleware.ts │ │ │ │ │ └── index.tsx │ │ │ │ ├── mime.tsx │ │ │ │ ├── partial.tsx │ │ │ │ ├── partial_insert.tsx │ │ │ │ ├── pg.tsx │ │ │ │ ├── qs.tsx │ │ │ │ ├── radix.tsx │ │ │ │ ├── redis.tsx │ │ │ │ ├── remote_island.tsx │ │ │ │ ├── stripe.tsx │ │ │ │ ├── supabase_pg.tsx │ │ │ │ ├── tailwind.tsx │ │ │ │ └── throw.tsx │ │ │ ├── static/ │ │ │ │ ├── foo.txt │ │ │ │ └── test_static/ │ │ │ │ ├── foo/ │ │ │ │ │ └── index.html │ │ │ │ └── foo.txt │ │ │ ├── utils.ts │ │ │ └── vite.config.ts │ │ ├── deno.json │ │ ├── src/ │ │ │ ├── client.ts │ │ │ ├── mod.ts │ │ │ ├── plugins/ │ │ │ │ ├── build_id.ts │ │ │ │ ├── client_entry.ts │ │ │ │ ├── client_snapshot.ts │ │ │ │ ├── deno.ts │ │ │ │ ├── dev_server.ts │ │ │ │ ├── patches/ │ │ │ │ │ ├── code_eval.ts │ │ │ │ │ ├── code_eval_test.ts │ │ │ │ │ ├── commonjs.ts │ │ │ │ │ ├── commonjs_test.ts │ │ │ │ │ ├── http_absolute.ts │ │ │ │ │ ├── http_absolute_test.ts │ │ │ │ │ ├── inline_env_vars.ts │ │ │ │ │ ├── inline_env_vars_test.ts │ │ │ │ │ ├── jsx_comment.ts │ │ │ │ │ ├── jsx_comment_test.ts │ │ │ │ │ ├── remove_polyfills.ts │ │ │ │ │ └── remove_polyfills_test.ts │ │ │ │ ├── patches.ts │ │ │ │ ├── server_entry.ts │ │ │ │ ├── server_snapshot.ts │ │ │ │ ├── shims/ │ │ │ │ │ ├── object.entries/ │ │ │ │ │ │ └── index.ts │ │ │ │ │ └── supports-color/ │ │ │ │ │ └── index.ts │ │ │ │ ├── shims.ts │ │ │ │ └── verify_imports.ts │ │ │ ├── shared.ts │ │ │ └── utils.ts │ │ └── tests/ │ │ ├── build_test.ts │ │ ├── dev_server_test.ts │ │ ├── fixtures/ │ │ │ ├── deno_global_island/ │ │ │ │ ├── islands/ │ │ │ │ │ └── Foo.tsx │ │ │ │ ├── main.ts │ │ │ │ ├── routes/ │ │ │ │ │ └── index.tsx │ │ │ │ └── vite.config.ts │ │ │ ├── deno_global_ssr/ │ │ │ │ ├── main.ts │ │ │ │ ├── routes/ │ │ │ │ │ └── index.tsx │ │ │ │ └── vite.config.ts │ │ │ ├── island_global_name/ │ │ │ │ ├── deno.json │ │ │ │ ├── islands/ │ │ │ │ │ └── Map.tsx │ │ │ │ ├── main.ts │ │ │ │ ├── routes/ │ │ │ │ │ └── index.tsx │ │ │ │ └── vite.config.ts │ │ │ ├── no_islands/ │ │ │ │ ├── main.ts │ │ │ │ ├── routes/ │ │ │ │ │ └── index.tsx │ │ │ │ └── vite.config.ts │ │ │ ├── no_routes/ │ │ │ │ ├── main.ts │ │ │ │ └── vite.config.ts │ │ │ ├── no_static/ │ │ │ │ ├── main.ts │ │ │ │ ├── routes/ │ │ │ │ │ └── index.tsx │ │ │ │ └── vite.config.ts │ │ │ ├── node_builtin/ │ │ │ │ ├── islands/ │ │ │ │ │ └── NodeIsland.tsx │ │ │ │ ├── main.ts │ │ │ │ ├── routes/ │ │ │ │ │ └── index.tsx │ │ │ │ └── vite.config.ts │ │ │ ├── remote_island/ │ │ │ │ ├── main.ts │ │ │ │ ├── routes/ │ │ │ │ │ └── index.tsx │ │ │ │ └── vite.config.ts │ │ │ ├── tailwind_app/ │ │ │ │ ├── assets/ │ │ │ │ │ └── style.css │ │ │ │ ├── client.ts │ │ │ │ ├── main.ts │ │ │ │ ├── routes/ │ │ │ │ │ ├── _app.tsx │ │ │ │ │ └── index.tsx │ │ │ │ └── vite.config.ts │ │ │ ├── tailwind_no_app/ │ │ │ │ ├── assets/ │ │ │ │ │ └── style.css │ │ │ │ ├── client.ts │ │ │ │ ├── main.ts │ │ │ │ ├── routes/ │ │ │ │ │ └── index.tsx │ │ │ │ └── vite.config.ts │ │ │ └── test_files_exclusion/ │ │ │ ├── deno.json │ │ │ ├── main.ts │ │ │ ├── routes/ │ │ │ │ ├── foo.test.ts │ │ │ │ ├── index.tsx │ │ │ │ └── index_test.tsx │ │ │ └── vite.config.ts │ │ └── test_utils.ts │ └── update/ │ ├── README.md │ ├── deno.json │ └── src/ │ ├── mod.ts │ ├── update.ts │ ├── update_test.ts │ └── utils.ts ├── tools/ │ ├── check_docs.ts │ ├── check_links.ts │ └── release.ts ├── versions.json └── www/ ├── README.md ├── assets/ │ └── styles.css ├── client.ts ├── components/ │ ├── CodeBlock.tsx │ ├── CodeWindow.tsx │ ├── CopyButton.tsx │ ├── DocsSidebar.tsx │ ├── FancyLink.tsx │ ├── FeatureIcons.tsx │ ├── Footer.tsx │ ├── Header.tsx │ ├── Icons.tsx │ ├── NavigationBar.tsx │ ├── PageSection.tsx │ ├── Projects.tsx │ ├── SideBySide.tsx │ ├── WaveTank.ts │ └── homepage/ │ ├── CTA.tsx │ ├── CodeExampleBox.tsx │ ├── DemoBox.tsx │ ├── DenoSection.tsx │ ├── ExampleArrow.tsx │ ├── FormsSection.tsx │ ├── Hero.tsx │ ├── IslandsSection.tsx │ ├── PartialsSection.tsx │ ├── RecipeDemo.tsx │ ├── RenderingSection.tsx │ ├── SectionHeading.tsx │ ├── Simple.tsx │ └── SocialProof.tsx ├── data/ │ ├── docs.ts │ └── showcase.json ├── deno.json ├── dev.ts ├── islands/ │ ├── Counter.tsx │ ├── FormSubmitDemo.tsx │ ├── LemonBottom.tsx │ ├── LemonDrop.tsx │ ├── LemonTop.tsx │ ├── SearchButton.tsx │ ├── TableOfContents.tsx │ ├── ThemeToggle.tsx │ └── VersionSelect.tsx ├── main.ts ├── main_test.ts ├── routes/ │ ├── _app.tsx │ ├── _error.tsx │ ├── _middleware.ts │ ├── docs/ │ │ ├── [...slug].tsx │ │ ├── _layout.tsx │ │ ├── _middleware.ts │ │ └── index.tsx │ ├── index.tsx │ ├── raw.ts │ ├── recipes/ │ │ ├── _layout.tsx │ │ ├── _middleware.ts │ │ ├── lemon-honey-tea.tsx │ │ ├── lemonade.tsx │ │ └── lemondrop.tsx │ ├── showcase-bak.tsx │ ├── showcase.tsx │ ├── thanks.tsx │ └── update.tsx ├── static/ │ ├── docsearch.css │ ├── google40caa9e535ae39e9.html │ ├── markdown.css │ └── prism.css ├── utils/ │ ├── markdown.ts │ ├── prism.ts │ ├── screenshot.ts │ ├── screenshot_test.ts │ └── state.ts └── vite.config.ts ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitattributes ================================================ # Use Unix line endings in all text files. * text=auto eol=lf ================================================ FILE: .github/CODE_OF_CONDUCT.md ================================================ # Contributor Covenant Code of Conduct ## Our Pledge We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. ## Our Standards Examples of behavior that contributes to a positive environment for our community include: - Demonstrating empathy and kindness toward other people - Being respectful of differing opinions, viewpoints, and experiences - Giving and gracefully accepting constructive feedback - Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience - Focusing on what is best not just for us as individuals, but for the overall community Examples of unacceptable behavior include: - The use of sexualized language or imagery, and sexual attention or advances of any kind - Trolling, insulting or derogatory comments, and personal or political attacks - Public or private harassment - Publishing others' private information, such as a physical or email address, without their explicit permission - Other conduct which could reasonably be considered inappropriate in a professional setting ## Enforcement Responsibilities Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. ## Scope This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at hello@lcas.dev. All complaints will be reviewed and investigated promptly and fairly. All community leaders are obligated to respect the privacy and security of the reporter of any incident. ## Enforcement Guidelines Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: ### 1. Correction **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. ### 2. Warning **Community Impact**: A violation through a single incident or series of actions. **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. ### 3. Temporary Ban **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. ### 4. Permanent Ban **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. **Consequence**: A permanent ban from any sort of public interaction within the community. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). [homepage]: https://www.contributor-covenant.org For answers to common questions about this code of conduct, see the FAQ at https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations. ================================================ FILE: .github/CONTRIBUTING.md ================================================ # Contributing Guidelines ## Submitting a pull request First, please be sure to ensure `deno task ok` is run and successfully passes. ================================================ FILE: .github/dependabot.yml ================================================ version: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: # Check for updates to GitHub Actions every week interval: "weekly" ================================================ FILE: .github/workflows/ci.yml ================================================ name: ci on: push: branches: [main] pull_request: branches: [main] jobs: test: runs-on: ${{ matrix.os }} timeout-minutes: 10 strategy: fail-fast: false matrix: deno: ["v2.x", "canary"] os: [macOS-latest, windows-latest, ubuntu-latest] include: - os: ubuntu-latest cache_path: ~/.cache/deno/ - os: macos-latest cache_path: ~/Library/Caches/deno/ - os: windows-latest cache_path: ~\AppData\Local\deno\ steps: - name: Checkout repo uses: actions/checkout@v4 - name: Setup Deno uses: denoland/setup-deno@v2 with: cache: true deno-version: ${{ matrix.deno }} - name: Install dependencies run: deno install - name: Verify formatting if: startsWith(matrix.os, 'ubuntu') && matrix.deno == 'v2.x' run: deno fmt --check - name: Run linter if: startsWith(matrix.os, 'ubuntu') && matrix.deno == 'v2.x' run: deno lint - name: Spell-check if: startsWith(matrix.os, 'ubuntu') && matrix.deno == 'v2.x' uses: crate-ci/typos@master - name: Type check project run: deno task check:types - name: Run tests run: deno task test - name: Check docs run: deno task check:docs - name: Build fresh.deno.dev if: startsWith(matrix.os, 'ubuntu') && matrix.deno == 'v2.x' run: deno task build-www ================================================ FILE: .github/workflows/deploy.yml ================================================ name: Deploy on: push: branches: [main] pull_request: branches: [main] jobs: deploy: name: Deploy runs-on: ubuntu-latest permissions: id-token: write # Needed for auth with Deno Deploy contents: read # Needed to clone the repository steps: - name: Clone repository uses: actions/checkout@v4 - name: Install Deno uses: denoland/setup-deno@v2 with: cache: true - name: Run install step run: "deno install" - name: Build step working-directory: ./www run: "deno task build" - name: Upload to Deno Deploy uses: denoland/deployctl@v1 # Skip publishing for forks if: github.repository_owner == 'denoland' with: project: "fresh" entrypoint: "server.js" root: "./www/_fresh/" ================================================ FILE: .github/workflows/post_publish.yml ================================================ name: post_publish on: release: types: [published] jobs: update-dl-version: name: update dl.deno.land version runs-on: ubuntu-22.04 if: github.repository == 'denoland/fresh' steps: - name: Checkout repo uses: actions/checkout@v4 - name: Authenticate with Google Cloud uses: google-github-actions/auth@v2 with: project_id: denoland credentials_json: ${{ secrets.GCP_SA_KEY }} export_environment_variables: true create_credentials_file: true - name: Setup gcloud uses: google-github-actions/setup-gcloud@v2 with: project_id: denoland - name: Upload version file to dl.deno.land run: | cat packages/fresh/deno.json | jq -r ".version" > release-latest.txt gsutil -h "Cache-Control: no-cache" cp release-latest.txt gs://dl.deno.land/fresh/release-latest.txt ================================================ FILE: .github/workflows/publish.yml ================================================ name: Publish JSR on: push: branches: - main jobs: publish: runs-on: ubuntu-latest permissions: contents: read id-token: write # Skip publishing for forks if: github.repository_owner == 'denoland' steps: - uses: actions/checkout@v4 - name: Install Deno uses: denoland/setup-deno@v2 with: cache: true - name: Install dependencies run: deno install - name: Publish run: deno publish ================================================ FILE: .gitignore ================================================ _fresh/ .vite/ vendor/ node_modules/ .docs/ .DS_Store tmp_* coverage/ vite-inspect/ .vite-inspect/ demo/dist/ ================================================ FILE: .vscode/extensions.json ================================================ { "recommendations": [ "denoland.vscode-deno" ] } ================================================ FILE: .vscode/settings.json ================================================ { "deno.enable": true, "deno.lint": true, "deno.codeLens.test": true, "deno.documentPreloadLimit": 2000, "editor.formatOnSave": true, "editor.defaultFormatter": "denoland.vscode-deno", "[typescriptreact]": { "editor.defaultFormatter": "denoland.vscode-deno" }, "[typescript]": { "editor.defaultFormatter": "denoland.vscode-deno" }, "[javascriptreact]": { "editor.defaultFormatter": "denoland.vscode-deno" }, "[javascript]": { "editor.defaultFormatter": "denoland.vscode-deno" }, "[markdown]": { "editor.defaultFormatter": "denoland.vscode-deno" }, "css.customData": [ ".vscode/tailwind.json" ], "[json]": { "editor.defaultFormatter": "denoland.vscode-deno" } } ================================================ FILE: .vscode/tailwind.json ================================================ { "version": 1.1, "atDirectives": [ { "name": "@tailwind", "description": "Use the `@tailwind` directive to insert Tailwind's `base`, `components`, `utilities` and `screens` styles into your CSS.", "references": [ { "name": "Tailwind Documentation", "url": "https://tailwindcss.com/docs/functions-and-directives#tailwind" } ] }, { "name": "@apply", "description": "Use the `@apply` directive to inline any existing utility classes into your own custom CSS. This is useful when you find a common utility pattern in your HTML that you’d like to extract to a new component.", "references": [ { "name": "Tailwind Documentation", "url": "https://tailwindcss.com/docs/functions-and-directives#apply" } ] }, { "name": "@responsive", "description": "You can generate responsive variants of your own classes by wrapping their definitions in the `@responsive` directive:\n```css\n@responsive {\n .alert {\n background-color: #E53E3E;\n }\n}\n```\n", "references": [ { "name": "Tailwind Documentation", "url": "https://tailwindcss.com/docs/functions-and-directives#responsive" } ] }, { "name": "@screen", "description": "The `@screen` directive allows you to create media queries that reference your breakpoints by **name** instead of duplicating their values in your own CSS:\n```css\n@screen sm {\n /* ... */\n}\n```\n…gets transformed into this:\n```css\n@media (min-width: 640px) {\n /* ... */\n}\n```\n", "references": [ { "name": "Tailwind Documentation", "url": "https://tailwindcss.com/docs/functions-and-directives#screen" } ] }, { "name": "@variants", "description": "Generate `hover`, `focus`, `active` and other **variants** of your own utilities by wrapping their definitions in the `@variants` directive:\n```css\n@variants hover, focus {\n .btn-brand {\n background-color: #3182CE;\n }\n}\n```\n", "references": [ { "name": "Tailwind Documentation", "url": "https://tailwindcss.com/docs/functions-and-directives#variants" } ] } ] } ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2021-2023 Luca Casonato Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ [Documentation](#-documentation) | [Getting started](#-getting-started) | [API Reference](https://deno.land/x/fresh?doc) # fresh The Fresh logo: a sliced lemon dripping with juice **Fresh** is a next generation web framework, built for speed, reliability, and simplicity. Some stand-out features: - Island based client hydration for maximum interactivity. - Zero runtime overhead: no JS is shipped to the client by default. - No configuration necessary. - TypeScript support out of the box. - File-system routing à la Next.js. ## 📖 Documentation The [documentation](https://fresh.deno.dev/docs/introduction) is available on [fresh.deno.dev](https://fresh.deno.dev/). ## 🚀 Getting started Install the latest [Deno CLI](https://deno.com/) version. You can scaffold a new project by running the Fresh init script. To scaffold a project run the following: ```sh deno run -Ar jsr:@fresh/init ``` Then navigate to the newly created project folder: ``` cd fresh-project ``` From within your project folder, start the development server using the `deno task` command: ``` deno task dev ``` Now open http://localhost:5173 in your browser to view the page. You make changes to the project source code and see them reflected in your browser. To deploy the project to the live internet, you can use [Deno Deploy](https://deno.com/deploy): 1. Push your project to GitHub. 2. [Create a Deno Deploy project.](https://console.deno.com/new) 3. Select your GitHub repository. 4. The project will be deployed to a public $project.$username.deno.net subdomain with no configuration necessary. For a more in-depth getting started guide, visit the [Getting Started](https://fresh.deno.dev/docs/getting-started) page in the Fresh docs. ## Contributing We appreciate your help! To contribute, please read our [contributing guideline](./.github/CONTRIBUTING.md). ## Adding your project to the showcase If you feel that your project would be helpful to other Fresh users, please consider putting your project on the [showcase](https://fresh.deno.dev/showcase). However, websites that are just for promotional purposes may not be listed. To take a screenshot, run the following command. ```sh deno task screenshot [url] [your-app-name] ``` Then add your site to [showcase.json](https://github.com/denoland/fresh/blob/main/www/data/showcase.json), preferably with source code on GitHub, but not required. ## Badges ![Made with Fresh](./www/static/fresh-badge.svg) ```md [![Made with Fresh](https://fresh.deno.dev/fresh-badge.svg)](https://fresh.deno.dev) ``` ```html Made with Fresh ``` ![Made with Fresh(dark)](./www/static/fresh-badge-dark.svg) ```md [![Made with Fresh](https://fresh.deno.dev/fresh-badge-dark.svg)](https://fresh.deno.dev) ``` ```html Made with Fresh ``` ## Hashtags Use the following hashtags in your social media posts that reference Fresh and as Topics in the About section of your GitHub repos that contain Fresh code. It will assure maximum visibility for your posts and code, and promote Fresh development ecosystem visibility. - #denofresh - #deno Github repo Topics will not include the hash symbol. ================================================ FILE: _typos.toml ================================================ [files] extend-exclude = [ "packages/fresh/tests/fixture_partials/routes/scroll_restoration/index.tsx", "www/static/fonts/FixelVariable.woff2", "www/static/fonts/FixelVariableItalic.woff2", "packages/fresh/tests/lorem_ipsum.txt", ] [default] extend-ignore-identifiers-re = ["Fixel"] ================================================ FILE: deno.json ================================================ { "vendor": true, "nodeModulesDir": "manual", "workspace": [ "./packages/*", "./www" ], "tasks": { "demo": "deno task --cwd=packages/plugin-vite demo", "demo:build": "deno task --cwd=packages/plugin-vite demo:build", "demo:start": "deno task --cwd=packages/plugin-vite demo:start", "test": "deno test -A --parallel", "www": "deno task --cwd=www dev", "build-www": "deno task --cwd=www build", "screenshot": "deno run -A www/utils/screenshot.ts", "check:types": "deno check --allow-import", "check:docs": "deno run -A tools/check_docs.ts", "ok": "deno fmt --check && deno lint && deno task check:types && deno task test", "test:www": "deno test -A www/main_test.*", "release": "deno run -A tools/release.ts" }, "exclude": [ "**/_fresh/*", "**/tmp/*", "*/tests_OLD/**", "**/vite.config.ts.*", "*/vite.config.ts.*", "vite.config.ts.*" ], "publish": { "include": [ "src/**", "deno.json", "README.md", "LICENSE", "www/static/fresh-badge.svg", "www/static/fresh-badge-dark.svg", "*.todo" ], "exclude": ["**/*_test.*", "src/__OLD/**", "*.todo", "**/tests/**"] }, "imports": { "@deno/doc": "jsr:@deno/doc@^0.172.0", "@deno/esbuild-plugin": "jsr:@deno/esbuild-plugin@^1.2.0", "@fresh/build-id": "jsr:@fresh/build-id@^1.0.0", "@std/cli": "jsr:@std/cli@^1.0.19", "@std/collections": "jsr:@std/collections@^1.1.2", "@std/dotenv": "jsr:@std/dotenv@^0.225.5", "@std/http": "jsr:@std/http@^1.0.15", "@std/uuid": "jsr:@std/uuid@^1.0.7", "@supabase/postgrest-js": "npm:@supabase/postgrest-js@^1.21.4", "@types/mime-db": "npm:@types/mime-db@^1.43.6", "@types/node": "npm:@types/node@^24.3.0", "@types/pg": "npm:@types/pg@^8.15.5", "@types/prismjs": "npm:@types/prismjs@^1.26.5", "docsearch": "https://esm.sh/@docsearch/js@3.5.2?target=es2020", "esbuild": "npm:esbuild@0.25.7", "esbuild-wasm": "npm:esbuild-wasm@0.25.7", "fresh": "jsr:@fresh/core@^2.0.0", "mime-db": "npm:mime-db@^1.54.0", "preact": "npm:preact@^10.28.2", "preact-render-to-string": "npm:preact-render-to-string@^6.6.5", "$ga4": "https://raw.githubusercontent.com/denoland/ga4/main/mod.ts", "@opentelemetry/api": "npm:@opentelemetry/api@^1.9.0", "@preact/signals": "npm:@preact/signals@^2.5.1", "@std/encoding": "jsr:@std/encoding@1", "@std/fmt": "jsr:@std/fmt@^1.0.7", "@std/fs": "jsr:@std/fs@1", "@std/html": "jsr:@std/html@1", "@std/jsonc": "jsr:@std/jsonc@1", "@std/media-types": "jsr:@std/media-types@1", "@std/path": "jsr:@std/path@1", "@std/semver": "jsr:@std/semver@1", "@std/streams": "jsr:@std/streams@1", "@astral/astral": "jsr:@astral/astral@^0.5.5", "@marvinh-test/fresh-island": "jsr:@marvinh-test/fresh-island@^0.0.3", "linkedom": "npm:linkedom@^0.18.10", "@std/async": "jsr:@std/async@^1.0.13", "@std/expect": "jsr:@std/expect@^1.0.16", "@std/testing": "jsr:@std/testing@^1.0.12", "@tailwindcss/postcss": "npm:@tailwindcss/postcss@^4.1.10", "redis": "npm:redis@^5.8.2", "rollup": "npm:rollup@^4.55.1", "tailwindcss": "npm:tailwindcss@^4.1.10", "postcss": "npm:postcss@8.5.6", "ts-morph": "npm:ts-morph@^26.0.0", "@std/front-matter": "jsr:@std/front-matter@^1.0.5", "github-slugger": "npm:github-slugger@^2.0.0", "imagescript": "https://deno.land/x/imagescript@1.3.0/mod.ts", "marked": "npm:marked@^15.0.11", "marked-mangle": "npm:marked-mangle@^1.1.9", "prismjs": "npm:prismjs@^1.29.0", "vite": "npm:vite@^7.3.1" }, "compilerOptions": { "lib": ["dom", "dom.asynciterable", "deno.ns", "deno.unstable"], "jsx": "precompile", "jsxImportSource": "preact", "jsxPrecompileSkipElements": [ "a", "img", "source", "body", "html", "head", "title", "meta", "script", "link", "style", "base", "noscript", "template" ], "types": ["vite/client"] }, "lint": { "rules": { "tags": ["recommended", "fresh", "jsr", "jsx", "react"], "exclude": ["no-window"], "include": ["no-console"] } }, "fmt": { "exclude": ["./www/static/**/*.svg"] } } ================================================ FILE: docs/1.x/concepts/ahead-of-time-builds.md ================================================ --- description: | Fresh optimize assets ahead of time, which makes pages load way quicker. --- Fresh enables you to pre-optimize frontend assets before the code is deployed. During that process the code for Islands will be compressed and optimized, so that Fresh can send as little code as possible to the browser. Depending on the amount of code an island needs, this process can take several seconds if done on the fly server-side. Doing those optimizations ahead-of-time and deploying the already optimized assets alongside with your code, allows Fresh to treat them as like any other static file and can serve it immediately without any further processing. On pages with islands, having to do no processing greatly speeds up page load times. Plugins can build static assets during ahead-of-time builds. This can be used to pre-process or generate CSS files, for example. ## Creating an optimized build To have Fresh optimize all the assets, run one of the following commands: ```sh Terminal # As a task in newer Fresh projects deno task build # or invoke it manually deno run -A dev.ts build ``` This will create a `_fresh` folder in the project directory. That folder contains the optimized assets and a `snapshot.json` file which includes some metadata for Fresh. Any other static files generated by plugins will be stored in the `_fresh/static` subfolder. They will be served the same as other [static files](/docs/1.x/concepts/static-files.md). > [info]: The `_fresh` folder should not be committed to the repository. Add an > entry in the `.gitignore` file to ensure that it is not committed. Create that > file at the root of your git repository if not present. > > ```gitignore .gitignore > # Ignore fresh build directory > _fresh/ > ``` ## Running Fresh with optimized assets When Fresh is started in non-development mode (usually via `main.ts`), Fresh will automatically pick up optimized assets when a `_fresh` folder exists. If found, Fresh will print the following message to the terminal: ```sh Terminal output Using snapshot found at /path/to/project/_fresh ``` ## Deploying an optimized Fresh project If you are deploying a Fresh project to Deno Deploy, you can use ahead-of-time builds to optimize the assets before deploying them. This will make your application load quicker. Open the Deno Deploy dashboard for your project and head to the "Git Integration" section in the project settings. Enter `deno task build` in the "Build command" field and save. This will switch your Deno Deploy project to use ahead-of-time builds. ## Migrating existing projects with Plugins If you're using Fresh plugins, extract them into a `fresh.config.ts` file, so that both the `dev.ts` and `main.ts` script have access to them. ```ts fresh.config.ts import { defineConfig } from "$fresh/server.ts"; import twindPlugin from "$fresh/plugins/twind.ts"; import twindConfig from "./twind.config.ts"; export default defineConfig({ plugins: [twindPlugin(twindConfig)], }); ``` ```ts main.ts import { start } from "$fresh/server.ts"; import manifest from "./fresh.gen.ts"; import config from "./fresh.config.ts"; await start(manifest, config); ``` ```ts dev.ts import dev from "$fresh/dev.ts"; import config from "./fresh.config.ts"; await dev(import.meta.url, "./main.ts", config); ``` ================================================ FILE: docs/1.x/concepts/app-wrapper.md ================================================ --- description: | Add a global app wrapper to provide common meta tags or context for application routes. --- An app wrapper is defined in an `_app.tsx` file in `routes/` folder and is typically used to create the outer structure of an HTML document. It must contain a default export that is a regular Preact component. Only one such wrapper is allowed per application. The component to be wrapped is received via props, in addition to a few other things. This allows for the introduction of a global container functioning as a template which can be conditioned based on state and params. Note that any state set by middleware is available via `props.state`. ```tsx routes/_app.tsx import { PageProps } from "$fresh/server.ts"; export default function App({ Component, state }: PageProps) { // do something with state here return ( My Fresh app ); } ``` ## Async app wrapper Similar to routes and layouts, the app wrapper can be made asynchronous. This changes the function signature so that the first argument is the `Request` instance and the second one is the `FreshContext`. ```tsx routes/_app.tsx import { FreshContext } from "$fresh/server.ts"; export default async function App(req: Request, ctx: FreshContext) { const data = await loadData(); return ( My Fresh app

Hello {data.name}

); } ``` ### Define helper To make it quicker to type the async app wrapper, Fresh includes a `defineApp` helper which already infers the correct types for you. ```tsx routes/_app.tsx import { defineApp } from "$fresh/server.ts"; export default defineApp(async (req, ctx) => { const data = await loadData(); return ( My Fresh app

Hello {data.name}

); }); ``` ## Disabling the app wrapper Rendering the app wrapper can be skipped on a route or layout basis. To do that, set `skipAppWrapper: true` to the layout or route config. ```tsx routes/my-special-route.tsx import { RouteConfig } from "$fresh/server.ts"; export const config: RouteConfig = { skipAppWrapper: true, // Skip the app wrapper during rendering }; export default function Page() { // ... } ``` ================================================ FILE: docs/1.x/concepts/architecture.md ================================================ --- description: | Fresh's architecture is designed to make it easy to build fast, scalable, and reliable applications. --- Fresh is designed to make it easy to build fast, scalable, and reliable applications. To do this, it makes opinionated decisions about how one should build web applications. These decisions are backed by strong empirical data gathered from experts in the field. Some examples of these principles are: - Page load times should be reduced to a minimum. - The work performed on the client should be minimized. - Errors should have a small blast radius - stuff should gracefully degrade. The single biggest architecture decision that Fresh makes is its usage of the [islands architecture][islands] pattern. This means that Fresh applications ship pure HTML to the client by default. Parts of a server-rendered page can then be independently re-hydrated with interactive widgets (islands). This means that the client is only responsible for rendering parts of the page that are interactive enough to warrant the extra effort. Any content that is purely static does not have related client-side JavaScript and is thus very lightweight. [islands]: https://www.patterns.dev/posts/islands-architecture/ ================================================ FILE: docs/1.x/concepts/data-fetching.md ================================================ --- description: | Data fetching in Fresh happens inside of route handler functions. These can pass route data to the page via page props. --- Server side data fetching in Fresh is accomplished through asynchronous handler functions. These handler functions can call a `ctx.render()` function with the data to be rendered as an argument. This data can then be retrieved by the page component through the `data` property on the `props`. Here is an example: ```tsx routes/projects/[id].tsx interface Project { name: string; stars: number; } export const handler: Handlers = { async GET(_req, ctx) { const project = await db.projects.findOne({ id: ctx.params.id }); if (!project) { return ctx.renderNotFound({ message: "Project does not exist", }); } return ctx.render(project); }, }; export default function ProjectPage(props: PageProps) { return (

{props.data.name}

{props.data.stars} stars

); } ``` The type parameter on the `PageProps`, `Handlers`, `Handler`, and `FreshContext` can be used to enforce a TypeScript type to use for the render data. Fresh enforces during type checking that the types in all of these fields are compatible within a single page. ## Asynchronous routes As a shortcut for combining a `GET` handler with a route, you can define your route as `async`. An `async` route (a route that returns a promise) will be called with the `Request` and a `RouteContext` (similar to a `HandlerContext`). Here is the above example rewritten using this shortcut: ```tsx routes/projects/[id].tsx interface Project { name: string; stars: number; } export default async function ProjectPage(_req, ctx: FreshContext) { const project: Project | null = await db.projects.findOne({ id: ctx.params.id, }); if (!project) { return

Project not found

; } return (

{project.name}

{project.stars} stars

); } ``` ================================================ FILE: docs/1.x/concepts/deployment.md ================================================ --- description: | Fresh can be deployed to a variety of platforms easily. --- While Fresh is designed to be deployed to [Deno Deploy][deno-deploy], it can be deployed to any system or platform that can run a Deno based web server. Here are instructions for specific providers / systems: - [Deno Deploy](#deno-deploy) - [Docker](#docker) - [Self Contained Executable](#self-contained-executable) ## Deno Deploy The recommended way to deploy Fresh is by using Deno Deploy. Deno Deploy provides a GitHub integration that can deploy your Fresh projects to its globally distributed edge network in seconds, automatically. View [the getting started guide][deploy-to-production] for instructions on how to deploy Fresh to Deno Deploy. ## Docker You can deploy Fresh to any platform that can run Docker containers. Docker is a tool to containerize projects and portably run them on any supported platform. When packaging your Fresh app for Docker, it is important that you set the `DENO_DEPLOYMENT_ID` environment variable in your container. This variable needs to be set to an opaque string ID that represents the version of your application that is currently being run. This could be a Git commit hash, or a hash of all files in your project. It is critical for the function of Fresh that this ID changes when _any_ file in your project changes - if it doesn't, incorrect caching **will** cause your project to not function correctly. Here is an example `Dockerfile` for a Fresh project: ```dockerfile Dockerfile FROM denoland/deno:1.38.3 ARG GIT_REVISION ENV DENO_DEPLOYMENT_ID=${GIT_REVISION} WORKDIR /app COPY . . RUN deno cache main.ts EXPOSE 8000 CMD ["run", "-A", "main.ts"] ``` To build your Docker image inside of a Git repository: ```sh Terminal $ docker build --build-arg GIT_REVISION=$(git rev-parse HEAD) -t my-fresh-app . ``` Then run your Docker container: ```sh Terminal $ docker run -t -i -p 80:8000 my-fresh-app ``` To deploy to a cloud provider, push it to a container registry and follow their documentation. - [Amazon Web Services][aws-container-registry] - [Google Cloud][gcp-container-registry] ## Self Contained Executable With Deno 2.1, you can create a self-contained executable of your Fresh project that includes all assets and dependencies. This executable can run on any platform without requiring Deno to be installed. ```sh Terminal $ deno task build $ deno compile --include static --include _fresh --include deno.json -A main.ts ``` [aws-container-registry]: https://docs.aws.amazon.com/AmazonECS/latest/userguide/create-container-image.html#create-container-image-push-ecr [gcp-container-registry]: https://cloud.google.com/container-registry/docs/pushing-and-pulling [deno-deploy]: https://deno.com/deploy [deploy-to-production]: /docs/1.x/getting-started/deploy-to-production ================================================ FILE: docs/1.x/concepts/error-pages.md ================================================ --- description: | Error pages can be used to customize the page that is shown when an error occurs in the application. --- Fresh supports customizing the `404 Not Found`, and the `500 Internal Server Error` pages. These are shown when a request is made but no matching route exists, and when a middleware, route handler, or page component throws an error respectively. ### 404: Not Found The 404 page can be customized by creating a `_404.tsx` file in the `routes/` folder. The file must have a default export that is a regular Preact component. A props object of type `PageProps` is passed in as an argument. ```tsx routes/_404.tsx import { PageProps } from "$fresh/server.ts"; export default function NotFoundPage({ url }: PageProps) { return

404 not found: {url.pathname}

; } ``` #### Manually render 404 pages The `_404.tsx` file will be invoked automatically when no route matches the URL. In some cases, one needs to manually trigger the rendering of the 404 page, for example when the route did match, but the requested resource does not exist. This can be achieved with `ctx.renderNotFound`. ```tsx routes/blog/[slug].tsx import { Handlers, PageProps } from "$fresh/server.ts"; export const handler: Handlers = { async GET(req, ctx) { const blogpost = await fetchBlogpost(ctx.params.slug); if (!blogpost) { return ctx.renderNotFound({ custom: "prop", }); } return ctx.render({ blogpost }); }, }; export default function BlogpostPage({ data }) { return (

{data.blogpost.title}

{/* rest of your page */}
); } ``` This can also be achieved by throwing an error, if you're uninterested in passing specific data to your 404 page: ```tsx routes/missing-page.tsx import { Handlers } from "$fresh/server.ts"; export const handler: Handlers = { GET(_req, _ctx) { throw new Deno.errors.NotFound(); }, }; ``` ### 500: Internal Server Error The 500 page can be customized by creating a `_500.tsx` file in the `routes/` folder. The file must have a default export that is a regular Preact component. A props object of type `PageProps` is passed in as an argument. ```tsx routes/_500.tsx import { PageProps } from "$fresh/server.ts"; export default function Error500Page({ error }: PageProps) { return

500 internal error: {(error as Error).message}

; } ``` ================================================ FILE: docs/1.x/concepts/forms.md ================================================ --- description: | Robustly handle user inputs using HTML `
` elements client side, and form submission handlers server side. --- For stronger resiliency and user experience, Fresh relies on native browser support for form submissions with the HTML `` element. In the browser, a `` submit will send an HTML action (usually `GET` or `POST`) to the server, which responds with a new page to render. ## POST request with `application/x-www-form-urlencoded` Forms typically submit as a `GET` request with data encoded in the URL's search parameters, or as a `POST` request with either an `application/x-www-form-urlencoded` or `multipart/form-data` body. This example demonstrates how to handle `application/x-www-form-urlencoded` `` submissions: ```tsx routes/subscribe.tsx import { Handlers } from "$fresh/server.ts"; export const handler: Handlers = { async GET(req, ctx) { return await ctx.render(); }, async POST(req, ctx) { const form = await req.formData(); const email = form.get("email")?.toString(); // Add email to list. // Redirect user to thank you page. const headers = new Headers(); headers.set("location", "/thanks-for-subscribing"); return new Response(null, { status: 303, // See Other headers, }); }, }; export default function Subscribe() { return ( <>
); } ``` When the user submits the form, Deno will retrieve the `email` value using the request's `formData()` method, add the email to a list, and redirect the user to a thank you page. ## Handling file uploads File uploads can be handled in a very similar manner to the example above. Note that this time, we have to explicitly declare the form's encoding to be `multipart/form-data`. ```tsx routes/subscribe.tsx import { Handlers, type PageProps } from "$fresh/server.ts"; interface Props { message: string | null; } export const handler: Handlers = { async GET(req, ctx) { return await ctx.render({ message: null, }); }, async POST(req, ctx) { const form = await req.formData(); const file = form.get("my-file") as File; if (!file) { return ctx.render({ message: `Please try again`, }); } const name = file.name; const contents = await file.text(); console.log(contents); return ctx.render({ message: `${name} uploaded!`, }); }, }; export default function Upload(props: PageProps) { const { message } = props.data; return ( <>
{message ?

{message}

: null} ); } ``` ## A note of caution These examples are simplified to demonstrate how Deno and Fresh handle HTTP requests. In the Real World™, you'll want to validate your data (_especially the file type_) and protect against cross-site request forgery. Consider yourself warned. ================================================ FILE: docs/1.x/concepts/index.md ================================================ --- description: | This chapter goes over some fundamental concepts of Fresh. --- This chapter goes over some fundamental concepts of Fresh. It covers the overarching architecture design of Fresh applications, as well as reference documentation about the various features of Fresh. ================================================ FILE: docs/1.x/concepts/islands.md ================================================ --- description: | Islands enable client side interactivity in Fresh. They are hydrated on the client in addition to being rendered on the server. --- Islands enable client side interactivity in Fresh. Islands are isolated Preact components that are rendered on the server and then hydrated on the client. This is different from all other components in Fresh, as they are usually rendered on the server only. Islands are defined by creating a file in the `islands/` folder in a Fresh project. The name of this file must be a PascalCase or kebab-case name of the island. ```tsx islands/my-island.tsx import { useSignal } from "@preact/signals"; export default function MyIsland() { const count = useSignal(0); return (
Counter is at {count}.{" "}
); } ``` An island can be used in a page like a regular Preact component. Fresh will take care of automatically re-hydrating the island on the client. ```tsx route/index.tsx import MyIsland from "../islands/my-island.tsx"; export default function Home() { return ; } ``` ## Passing JSX to islands Islands support passing JSX elements via the `children` property. ```tsx islands/my-island.tsx import { useSignal } from "@preact/signals"; import { ComponentChildren } from "preact"; interface Props { children: ComponentChildren; } export default function MyIsland({ children }: Props) { const count = useSignal(0); return (
Counter is at {count}.{" "} {children}
); } ``` This allows you to pass static content rendered by the server to an island in the browser. ```tsx routes/index.tsx import MyIsland from "../islands/my-island.tsx"; export default function Home() { return (

This text is rendered on the server

); } ``` You can also create shared components in your `components/` directory, which can be used in both static content and interactive islands. When these components are used within islands, interactivity can be added, such as `onClick` handlers (using an `onClick` handler on a button outside of an island will not fire). ```tsx islands/my-island.tsx import { useSignal } from "@preact/signals"; import { ComponentChildren } from "preact"; import Card from "../components/Card.tsx"; import Button from "../components/Button.tsx"; interface Props { children: ComponentChildren; } export default function MyIsland({ children }: Props) { const count = useSignal(0); return ( Counter is at {count}.{" "} {children} ); } ``` ## Passing other props to islands Passing props to islands is supported, but only if the props are serializable. Fresh can serialize the following types of values: - Primitive types `string`, `boolean`, `bigint`, and `null` - Most `number`s (`Infinity`, `-Infinity`, and `NaN` are silently converted to `null`) - Plain objects with string keys and serializable values - Arrays containing serializable values - Uint8Array - JSX Elements (restricted to `props.children`) - Preact Signals (if the inner value is serializable) Circular references are supported. If an object or signal is referenced multiple times, it is only serialized once and the references are restored upon deserialization. Passing complex objects like `Date`, custom classes, or functions is not supported. During server side rendering, Fresh annotates the HTML with special comments that indicate where each island will go. This gives the code sent to the client enough information to put the islands where they are supposed to go without requiring hydration for the static children of interactive islands. No Javascript is sent to the client when no interactivity is needed. ```html Response body
Counter is at 0.

This text is rendered on the server

``` ### Nesting islands Islands can be nested within other islands as well. In that scenario they act like a normal Preact component, but still receive the serialized props if any were present. ```tsx islands/other-island.tsx import { useSignal } from "@preact/signals"; import { ComponentChildren } from "preact"; interface Props { children: ComponentChildren; foo: string; } function randomNumber() { return Math.floor(Math.random() * 100); } export default function OtherIsland({ children, foo }: Props) { const number = useSignal(randomNumber()); return (

String from props: {foo}

{" "} number is: {number}.

); } ``` In essence, Fresh allows you to mix static and interactive parts in your app in a way that's most optimal for your app. We'll keep sending only the JavaScript that is needed for the islands to the browser. ```tsx route/index.tsx import MyIsland from "../islands/my-island.tsx"; import OtherIsland from "../islands/other-island.tsx"; export default function Home() { return (

Some more server rendered text

); } ``` ## Rendering islands on client only When using client-only APIs, like `EventSource` or `navigator.getUserMedia`, this component will not run on the server as it will produce an error like: ``` An error occurred during route handling or page rendering. ReferenceError: EventSource is not defined at Object.MyIsland (file:///Users/someuser/fresh-project/islandsmy-island.tsx:6:18) at m (https://esm.sh/v129/preact-render-to-string@6.2.0/X-ZS8q/denonext/preact-render-to-string.mjs:2:2602) at m (https://esm.sh/v129/preact-render-to-string@6.2.0/X-ZS8q/denonext/preact-render-to-string.mjs:2:2113) .... ``` Use the [`IS_BROWSER`](https://deno.land/x/fresh/runtime.ts?doc=&s=IS_BROWSER) flag as a guard to fix the issue: ```tsx islands/my-island.tsx import { IS_BROWSER } from "$fresh/runtime.ts"; export function MyIsland() { // Return any prerenderable JSX here which makes sense for your island if (!IS_BROWSER) return
; // All the code which must run in the browser comes here! // Like: EventSource, navigator.getUserMedia, etc. return
; } ``` ================================================ FILE: docs/1.x/concepts/layouts.md ================================================ --- description: | Add a layout to provide common meta tags, context for application sub routes, and common layout. --- A layout is defined in a `_layout.tsx` file in any sub directory (at any level) under the `routes/` folder. It must contain a default export that is a regular Preact component. Only one such layout is allowed per sub directory. ```txt-files Project structure └── routes    ├── sub    │ ├── page.tsx    │   └── index.tsx ├── other │ ├── _layout.tsx # will be applied on top of `routes/_layout.tsx` │ └── page.tsx ├── _layout.tsx # will be applied to all routes └── _app.tsx ``` The component to be wrapped is received via props, in addition to a few other things. This allows for the introduction of a global container functioning as a template which can be conditioned based on state and params. Note that any state set by middleware is available via `props.state`. ```tsx routes/sub/_layout.tsx import { PageProps } from "$fresh/server.ts"; export default function Layout({ Component, state }: PageProps) { // do something with state here return (
); } ``` ## Async layouts In case you need to fetch data asynchronously before rendering the layout, you can use an async layout to do so. ```tsx routes/sub/_layout.tsx import { FreshContext } from "$fresh/server.ts"; export default async function Layout(req: Request, ctx: FreshContext) { // do something with state here const data = await loadData(); return (

{data.greeting}

); } ``` ### Define helper To make it a little quicker to write async layouts, Fresh ships with a `defineLayout` helper which automatically infers the correct types for the function arguments. ```tsx routes/greet/_layout.tsx import { defineLayout } from "$fresh/server.ts"; export default defineLayout(async (req, ctx) => { const data = await loadData(); return (

{data.greeting}

); }); ``` ## Opting out of layout inheritance Sometimes you want to opt out of the layout inheritance mechanism for a particular route. This can be done via route configuration. Picture a directory structure like this: ```txt-files Project structure └── /routes    ├── sub    │ ├── _layout_.tsx    │ ├── special.tsx # should not inherit layouts    │   └── index.tsx └── _layout.tsx ``` To make `routes/sub/special.tsx` opt out of rendering layouts we can set `skipInheritedLayouts: true`. ```tsx routes/sub/special.tsx import { RouteConfig } from "$fresh/server.ts"; export const config: RouteConfig = { skipInheritedLayouts: true, // Skip already inherited layouts }; export default function MyPage() { return

Hello world

; } ``` You can skip already inherited layouts inside a layout file: ```tsx routes/special/_layout.tsx import { LayoutConfig } from "$fresh/server.ts"; export const config: LayoutConfig = { skipInheritedLayouts: true, // Skip already inherited layouts }; export default function MyPage() { return

Hello world

; } ``` ================================================ FILE: docs/1.x/concepts/middleware.md ================================================ --- description: | Add middleware routes to intercept requests or responses for analytics purposes, access control, or anything else. --- A middleware is defined in a `_middleware.ts` file. It will intercept the request in order for you to perform custom logic before or after the route handler. This allows modifying or checking requests and responses. Common use-cases for this are logging, authentication, and performance monitoring. Each middleware gets passed a `next` function in the context argument that is used to trigger child handlers. The `ctx` also has a `state` property that can be used to pass arbitrary data to downstream (or upstream) handlers. This `state` is included in `PageProps` by default, which is available to both the special [\_app](/docs/1.x/concepts/app-wrapper.md) wrapper and normal [routes](/docs/1.x/concepts/routes.md). `ctx.state` is normally set by modifying its properties, e.g. `ctx.state.loggedIn = true`, but you can also replace the entire object like `ctx.state = { loggedIn: true }`. ```ts routes/_middleware.ts import { FreshContext } from "$fresh/server.ts"; interface State { data: string; } export async function handler( req: Request, ctx: FreshContext, ) { ctx.state.data = "myData"; const resp = await ctx.next(); resp.headers.set("server", "fresh server"); return resp; } ``` ```ts routes/myHandler.ts export const handler: Handlers = { GET(_req, ctx) { return new Response(`middleware data is ${ctx.state.data}`); }, }; ``` Middlewares are scoped and can be layered. This means a project can have multiple middlewares, each covering a different set of routes. If multiple middlewares cover a route, they will all be run, in order of specificity (least specific first). For example, take a project with the following routes: ```txt-files Project Structure └── /routes    ├── _middleware.ts    ├── index.ts    └── admin       ├── _middleware.ts       └── index.ts       └── signin.ts ``` For a request to `/` the request will flow like this: 1. The `routes/_middleware.ts` middleware is invoked. 2. Calling `ctx.next()` will invoke the `routes/index.ts` handler. For a request to `/admin` the request flows like this: 1. The `routes/_middleware.ts` middleware is invoked. 2. Calling `ctx.next()` will invoke the `routes/admin/_middleware.ts` middleware. 3. Calling `ctx.next()` will invoke the `routes/admin/index.ts` handler. For a request to `/admin/signin` the request flows like this: 1. The `routes/_middleware.ts` middleware is invoked. 2. Calling `ctx.next()` will invoke the `routes/admin/_middleware.ts` middleware. 3. Calling `ctx.next()` will invoke the `routes/admin/signin.ts` handler. A single middleware file can also define multiple middlewares (all for the same route) by exporting an array of handlers instead of a single handler. For example: ```ts routes/_middleware.ts export const handler = [ async function middleware1(req, ctx) { // do something return ctx.next(); }, async function middleware2(req, ctx) { // do something return ctx.next(); }, ]; ``` It should be noted that `middleware` has access to route parameters. If you're running a fictitious `routes/[tenant]/admin/_middleware.ts` like this: ```ts routes/[tenant]/admin/_middleware.ts import { FreshContext } from "$fresh/server.ts"; export async function handler(_req: Request, ctx: FreshContext) { const currentTenant = ctx.params.tenant; // do something with the tenant const resp = await ctx.next(); return resp; } ``` and the request is to `mysaas.com/acme/admin/`, then `currentTenant` will have the value of `acme` in your middleware. ## Middleware Destination To set the stage for this section, let's focus on the part of `FreshContext` that looks like this: ```ts fresh 🍋 export interface FreshContext> { ... next: () => Promise; state: State; destination: router.DestinationKind; remoteAddr: { transport: "tcp" | "udp"; hostname: string; port: number; }; ... } ``` and `router.DestinationKind` is defined like this: ```ts fresh 🍋 export type DestinationKind = "internal" | "static" | "route" | "notFound"; ``` This is useful if you want your middleware to only run when a request is headed for a `route`, as opposed to something like `http://localhost:8001/favicon.ico`. ### Example Initiate a new Fresh project (`deno run -A -r https://fresh.deno.dev/`) and then create a `_middleware.ts` file in the `routes` folder like this: ```ts routes/_middleware.ts import { FreshContext } from "$fresh/server.ts"; export async function handler(req: Request, ctx: FreshContext) { console.log(ctx.destination); console.log(req.url); const resp = await ctx.next(); return resp; } ``` If you start up your server (`deno task start`) you'll see the following: ```sh Terminal Task start deno run -A --watch=static/,routes/ dev.ts Watcher Process started. The manifest has been generated for 4 routes and 1 islands. 🍋 Fresh ready Local: http://localhost:8000/ route http://localhost:8000/ internal http://localhost:8000/_frsh/js/3c7400558fc00915df88cb181036c0dbf73ab7f5/deserializer.js internal http://localhost:8000/_frsh/js/3c7400558fc00915df88cb181036c0dbf73ab7f5/signals.js internal http://localhost:8000/_frsh/js/3c7400558fc00915df88cb181036c0dbf73ab7f5/plugin-twind-main.js internal http://localhost:8000/_frsh/js/3c7400558fc00915df88cb181036c0dbf73ab7f5/main.js internal http://localhost:8000/_frsh/js/3c7400558fc00915df88cb181036c0dbf73ab7f5/island-counter.js internal http://localhost:8000/_frsh/refresh.js static http://localhost:8000/logo.svg?__frsh_c=3c7400558fc00915df88cb181036c0dbf73ab7f5 internal http://localhost:8000/_frsh/alive internal http://localhost:8000/_frsh/js/3c7400558fc00915df88cb181036c0dbf73ab7f5/chunk-PDMKJVJ5.js internal http://localhost:8000/_frsh/js/3c7400558fc00915df88cb181036c0dbf73ab7f5/chunk-UGFDDSOV.js internal http://localhost:8000/_frsh/js/3c7400558fc00915df88cb181036c0dbf73ab7f5/chunk-RCK7U3UF.js ``` That first `route` request is for when `Fresh` responds with the root level `index.tsx` route. The rest, as you can see, are either `internal` or `static` requests. You can use `ctx.destination` to filter these out if your middleware is only supposed to deal with routes. ## Middleware Redirects If you want to redirect a request from a middleware, you can do so by returning: ```ts routes/_middleware.ts export function handler(req: Request): Response { return Response.redirect("https://example.com", 307); } ``` `307` stands for temporary redirect. You can also use `301` for permanent redirect. You can also redirect to a relative path by doing: ```ts routes/_middleware.ts export function handler(req: Request): Response { return new Response("", { status: 307, headers: { Location: "/my/new/relative/path" }, }); } ``` ================================================ FILE: docs/1.x/concepts/partials.md ================================================ --- description: | Partials allow areas of a page to be updated without causing the browser to reload the page. They enable optimized fine grained UI updates and can be used to do client-side navigation. --- Partials allow areas of the page to be updated with new content by the server without causing the browser to reload the page. They make your website feel more app-like because only the parts of the page that need to be updated will be updated. ## Enabling partials Partials are enabled by adding a `f-client-nav` attribute to an HTML element and wrapping one or more areas in the page with a ``-component. The quickest way to get started is to enable partials for every page in `routes/_app.tsx` by making the following changes. ```diff routes/_app.tsx import { PageProps } from "$fresh/server.ts"; + import { Partial } from "$fresh/runtime.ts"; export default function App({ Component }: PageProps) { return ( My Fresh app - + + + ); } ``` By adding the `f-client-nav` attribute, we enable partials for every element beneath the ``-tag. To mark an area of the page as a partial we wrap it with a ``-component with a unique name. Behind the scenes, when the user clicks an ``-tag, Fresh fetches the new page and only pulls out the relevant content out of the HTML response. When it finds a matching partial area it will update the content inside the partial. > [info]: The `name` prop of the `` component is expected to be unique > among Partials. That's how Fresh knows which parts of the response need to go > on the current page. > [info]: Passing `f-client-nav={false}` disables client side navigation for all > elements below the current node. ### Optimizing partial requests By default, with `f-client-nav` set, Fresh fetches the full next page and only picks out the relevant parts of the response. We can optimize this pattern further by only rendering the parts we need, instead of always rendering the full page. This is done by adding the `f-partial` attribute to a link. ```diff routes/_app.tsx - Routes + Routes ``` When the `f-partial` attribute is present, Fresh will navigate to the page URL defined in the `href` attribute, but fetch the updated UI from the URL specified in `f-partial` instead. This can be a highly optimized route that only delivers the content you care about. Let's use a typical documentation page layout as an example. It often features a main content area and a sidebar of links to switch between pages of the documentation (marked green here). ![A sketched layout of a typical documentation page with the sidebar on the left composed of green links and a main content area on the right. The main content area is labeled as Partial docs-content](/docs/1.x/fresh-partial-docs.png) The code for such a page (excluding styling) might look like this: ```tsx routes/docs/[id].tsx export default defineRoute(async (req, ctx) => { const content = await loadContent(ctx.params.id); return (
{content}
); }); ``` An optimal route that only renders the content instead of the outer layout with the sidebar might look like this respectively. ```tsx routes/partials/docs/[id].tsx import { defineRoute, RouteConfig } from "$fresh/server.ts"; import { Partial } from "$fresh/runtime.ts"; // We only want to render the content, so disable // the `_app.tsx` template as well as any potentially // inherited layouts export const config: RouteConfig = { skipAppWrapper: true, skipInheritedLayouts: true, }; export default defineRoute(async (req, ctx) => { const content = await loadContent(ctx.params.id); // Only render the new content return ( {content} ); }); ``` By adding the `f-partial` attribute we tell Fresh to fetch the content from our newly added `/partials/docs/[id].tsx` route. ```diff routes/docs/[id].tsx ``` With this in place, Fresh will navigate to the new page when clicking any of the two links and _only_ load the content rendered by our optimized partial route. > Currently, `f-partial` is scoped to ``, ` ); }); ``` When the user submits the form, Deno will retrieve the `email` value using the request's `formData()` method, add the email to a list, and redirect the user to a thank you page. ## Handling file uploads File uploads can be handled in a very similar manner to the example above. Note that this time, we have to explicitly declare the form's encoding to be `multipart/form-data`. ```tsx routes/subscribe.tsx import { define } from "../utils.ts"; export const handler = define.handlers({ async GET(ctx) { return { data: { message: null } }; }, async POST(ctx) { const form = await ctx.req.formData(); const file = form.get("my-file") as File; if (!file) { return { data: { message: "Please try again" } }; } const name = file.name; const contents = await file.text(); console.log(contents); return { data: { message: `${name} uploaded!` } }; }, }); export default define.page(function Upload(props) { const { message } = props.data; return ( <>
{message ?

{message}

: null} ); }); ``` ## A note of caution These examples are simplified to demonstrate how Deno and Fresh handle HTTP requests. In the Real World™, you'll want to validate your data (_especially the file type_) and protect against cross-site request forgery. Consider yourself warned. ================================================ FILE: docs/latest/advanced/head.md ================================================ --- description: Modify the document head in Fresh --- The [``](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/head)-element is a crucial element in HTML to set metadata for a page. It allows you to: - Set the document title with `` - Specify page metadata with `<meta>` - Link to resources like stylesheets with `<link>` - Include JavaScript code with `<script>` > [info]: The outer HTML structure including `<head>` is typically created > inside `_app.tsx`. ## Passing metadata from `ctx.state` For simple scenarios passing metadata along from a handler or a middleware by writing to `ctx.state` is often sufficient. ```tsx routes/_app.tsx import { define } from "../util.ts"; export default define.page((ctx) => { return ( <html lang="en"> <head> <meta charset="utf-8" /> <title>{ctx.state.title ?? "Welcome!"} ); }); ``` ## Using the ``-component For more complex scenarios, or to set page metadata from islands, Fresh ships with the ``-component. > [info]: The `` component is not dynamic by default. It will not > automatically update the document title or other head elements on the client > side when component state changes. The head elements are set during server > rendering or initial page load. ```tsx routes/about.tsx import { Head } from "fresh/runtime"; export default define.page((ctx) => { return (
About me

About me

I like Fresh!

); }); ``` ### Avoiding duplicate tags You might end up with duplicate tags, when multiple `` components are rendered on the same page. Fresh will employ the following strategies to find the matching element: 1. For `` elements Fresh will set `document.title` directly 2. Check if an element with the same `key` exists 3. Check if an element with the same `id` attribute 4. Only for `<meta>` elements: Check if there is a `<meta>` element with the same `name` attribute 5. No matching element was found, Fresh will create a new one and append it to `<head>` > [info]: The `<title>`-tag is automatically deduplicated, even without a `key` > prop. ================================================ FILE: docs/latest/advanced/index.md ================================================ --- description: | This chapter goes over some advanced concepts of Fresh. --- This section of the documentation describes advanced functionality of Fresh. ================================================ FILE: docs/latest/advanced/layouts.md ================================================ --- description: "Create re-usable layouts across routes" --- Layouts are plain Preact components that are inherited based on the matching pattern. When you have a section on your site where all pages share the same HTML structure and only the content changes, a layout is a neat way to abstract this. Layouts only ever render on the server. The passed `Component` value represents the children of this component. ```tsx main.tsx function PageLayout({ Component }) { return ( <div> <Component /> <aside>Here is some sidebar content</aside> </div> ); } const app = new App() .layout("*", PageLayout) .get("/", (ctx) => ctx.render(<h1>hello</h1>)); ``` If you browse to the `/` route, Fresh will render the following HTML ```html Response body <div> <h1>hello world</h1> <aside>Here is some sidebar content</aside> </div> ``` ## Options Add a layout and ignore all previously inherited ones. ```ts main.ts app.layout("/foo/bar", MyComponent, { skipInheritedLayouts: true }); ``` Ignore the app wrapper component: ```ts main.ts app.layout("/foo/bar", MyComponent, { skipAppWrapper: true }); ``` ================================================ FILE: docs/latest/advanced/partials.md ================================================ --- description: | Partials allow areas of a page to be updated without causing the browser to reload the page. They enable optimized fine grained UI updates and can be used to do client-side navigation. --- Partials allow areas of the page to be updated with new content by the server without causing the browser to reload the page. They make your website feel more app-like because only the parts of the page that need to be updated will be updated. ## Enabling partials Partials are enabled by adding a `f-client-nav` attribute to an HTML element and wrapping one or more areas in the page with a `<Partial name="my-partial">`-component. The quickest way to get started is to enable partials for every page in `routes/_app.tsx` by making the following changes. ```diff routes/_app.tsx import { define } from "../utils.ts"; + import { Partial } from "fresh/runtime"; export default define.page(function App({ Component }) { return ( <html> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>My Fresh app - + + + ); }); ``` By adding the `f-client-nav` attribute, we enable partials for every element beneath the ``-tag. To mark an area of the page as a partial we wrap it with a ``-component with a unique name. Behind the scenes, when the user clicks an ``-tag, Fresh fetches the new page and only pulls out the relevant content out of the HTML response. When it finds a matching partial area it will update the content inside the partial. > [info]: The `name` prop of the `` component is expected to be unique > among Partials. That's how Fresh knows which parts of the response need to go > on the current page. > [info]: Passing `f-client-nav={false}` disables client side navigation for all > elements below the current node. ### Optimizing partial requests By default, with `f-client-nav` set, Fresh fetches the full next page and only picks out the relevant parts of the response. We can optimize this pattern further by only rendering the parts we need, instead of always rendering the full page. This is done by adding the `f-partial` attribute to a link. ```diff routes/_app.tsx - Routes + Routes ``` When the `f-partial` attribute is present, Fresh will navigate to the page URL defined in the `href` attribute, but fetch the updated UI from the URL specified in `f-partial` instead. This can be a highly optimized route that only delivers the content you care about. Let's use a typical documentation page layout as an example. It often features a main content area and a sidebar of links to switch between pages of the documentation (marked green here). ![A sketched layout of a typical documentation page with the sidebar on the left composed of green links and a main content area on the right. The main content area is labeled as Partial docs-content](/docs/fresh-partial-docs.png) The code for such a page (excluding styling) might look like this: ```tsx routes/docs/[id].tsx import { define } from "../../utils.ts"; export default define.page(async (ctx) => { const content = await loadContent(ctx.params.id); return (
{content}
); }); ``` An optimal route that only renders the content instead of the outer layout with the sidebar might look like this respectively. ```tsx routes/partials/docs/[id].tsx import { define } from "../utils.ts"; import { Partial } from "fresh/runtime"; // We only want to render the content, so disable // the `_app.tsx` template as well as any potentially // inherited layouts export const config: RouteConfig = { skipAppWrapper: true, skipInheritedLayouts: true, }; export default define.page(async (ctx) => { const content = await loadContent(ctx.params.id); // Only render the new content return ( {content} ); }); ``` By adding the `f-partial` attribute we tell Fresh to fetch the content from our newly added `/partials/docs/[id].tsx` route. ```diff routes/docs/[id].tsx ``` With this in place, Fresh will navigate to the new page when clicking any of the two links and _only_ load the content rendered by our optimized partial route. > Currently, `f-partial` is scoped to ``, ` ); } ``` An island can be used anywhere like a regular Preact component. Fresh will take care of making it interactive on the client. ```tsx main.tsx import { App, staticFiles } from "fresh"; import MyIsland from "./islands/my-island.tsx"; const app = new App() .use(staticFiles()) .get("/", (ctx) => ctx.render()); ``` ## Passing props to islands Passing props to islands is supported, but only if the props are serializable. Fresh can serialize the following types of values: - Primitive types `string`, `number`, `boolean`, `bigint`, `undefined`, and `null` - `Infinity`, `-Infinity`, `-0`, and `NaN` - `Uint8Array` - `URL` - `Date` - `RegExp` - `JSX` Elements - Collections `Map` and `Set` - Plain objects with string keys and serializable values - Arrays containing serializable values - Preact Signals (if the inner value is serializable) Circular references are supported. If an object or signal is referenced multiple times, it is only serialized once and the references are restored upon deserialization. > [warn]: Passing functions to an island is not supported. > > ```tsx routes/example.tsx > export default function () { > // WRONG > return console.log("hey")} />; > } > ``` ### Passing JSX A powerful feature of Fresh is that you can pass server-rendered JSX to an island via props. ```tsx routes/index.tsx import { staticFiles } from "fresh"; import MyIsland from "../islands/my-island.tsx"; const app = new App() .use(staticFiles()) .get("/", (ctx) => { return ctx.render( hello}>

This text is rendered on the server

, ); }); ``` ### Nesting islands Islands can be nested within other islands as well. In that scenario they act like a normal Preact component, but still receive the serialized props if any were present. In essence, Fresh allows you to mix static and interactive parts in your app in a way that's most optimal for your app. We'll keep sending only the JavaScript that is needed for the islands to the browser. ```tsx islands/other-island.tsx export default (props: { foo: string }) => <>{props.foo}; ``` ```tsx route/index.tsx import MyIsland from "../islands/my-island.tsx"; import OtherIsland from "../islands/other-island.tsx"; // Later...

Some more server rendered text

; ``` ## Rendering islands on client only When using client-only APIs, like `EventSource` or `navigator.getUserMedia`, this component will not run on the server as it will produce an error. To fix this use the `IS_BROWSER` flag as a guard: ```tsx islands/my-island.tsx import { IS_BROWSER } from "fresh/runtime"; export function MyIsland() { // Return any prerenderable JSX here which makes sense for your island if (!IS_BROWSER) return
; // All the code which must run in the browser comes here! // Like: EventSource, navigator.getUserMedia, etc. return
; } ``` ================================================ FILE: docs/latest/concepts/layouts.md ================================================ --- description: | Add a layout to provide common meta tags, context for application sub routes, and common layout. --- A layout is defined in a `_layout.tsx` file in any sub directory (at any level) under the `routes/` folder. It must contain a default export that is a regular Preact component. Only one such layout is allowed per sub directory. ```txt-files Project structure └── routes    ├── sub    │ ├── page.tsx    │   └── index.tsx ├── other │ ├── _layout.tsx # will be applied on top of `routes/_layout.tsx` │ └── page.tsx ├── _layout.tsx # will be applied to all routes └── _app.tsx ``` The component to be wrapped is received via props, in addition to a few other things. This allows for the introduction of a global container functioning as a template which can be conditioned based on state and params. Note that any state set by middleware is available via `props.state`. ```tsx routes/sub/_layout.tsx import { define } from "../../utils.ts"; export default define.layout({ Component, state }) => { // do something with state here return (
); }) ``` ## Async layouts In case you need to fetch data asynchronously before rendering the layout, you can use an async layout to do so. ```tsx routes/sub/_layout.tsx import { define } from "../../utils.ts"; export default define.layout(async (ctx) => { // do something with state here const data = await loadData(); return (

{data.greeting}

); }); ``` ## Opting out of layout inheritance Sometimes you want to opt out of the layout inheritance mechanism for a particular route. This can be done via route configuration. Picture a directory structure like this: ```txt-files Project structure └── /routes    ├── sub    │ ├── _layout_.tsx    │ ├── special.tsx # should not inherit layouts    │   └── index.tsx └── _layout.tsx ``` To make `routes/sub/special.tsx` opt out of rendering layouts we can set `skipInheritedLayouts: true`. ```tsx routes/sub/special.tsx import { type RouteConfig } from "fresh"; import { define } from "../../utils.ts"; export const config: RouteConfig = { skipInheritedLayouts: true, // Skip already inherited layouts }; export default define.layout(() => { return

Hello world

; }); ``` You can skip already inherited layouts inside a layout file: ```tsx routes/special/_layout.tsx import { type LayoutConfig } from "fresh"; export const config: LayoutConfig = { skipInheritedLayouts: true, // Skip already inherited layouts }; export default function MyPage() { return

Hello world

; } ``` ================================================ FILE: docs/latest/concepts/middleware.md ================================================ --- description: | Add middleware routes to intercept requests or responses for analytics purposes, access control, or anything else. --- A middleware is a function that receives a [`Context`](/docs/concepts/context) object with the [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) and returns a [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response). They are typically used to set HTTP Headers, measure response times or fetch data and pass it to another middleware. ```tsx main.ts const app = new App<{ greeting: string }>() .use((ctx) => { // Middleware to pass data ctx.state.greeting = "Hello world"; return ctx.next(); }) .use(async (ctx) => { // Middleware to add a HTTP header const res = await ctx.next(); res.headers.set("server", "fresh server"); return res; }) // A handler is a form of middleware that responds. Here we // render HTML and return it. .get("/", (ctx) => { return ctx.render(

{ctx.state.greeting}

); }); ``` Middlewares can be chained and combined in whatever way you desire. They are an excellent way to make http-related logic reusable on the server. ## Middleware helper Use the `define.middleware()` helper to get typings out of the box: ```ts middleware/my-middleware.ts import { define } from "../utils.ts"; const middleware = define.middleware(async (ctx) => { console.log("my middleware"); return await ctx.next(); }); ``` ## Included middlewares Fresh ships with the following middlewares built-in: - [cors()](/docs/plugins/cors) - Set CORS HTTP headers - [csrf()](/docs/plugins/csrf) - Set CSRF HTTP headers - [trailingSlash()](/docs/plugins/trailing-slashes) - Enforce trailing slashes ## Filesystem-based middlewares With file system based routing you can define a middleware in a `_middleware.ts` file inside the `routes/` folder or any of it's subfolders. ```ts routes/_middleware.ts import { define } from "../utils.ts"; export default define.middleware(async (ctx) => { console.log("my middleware"); return await ctx.next(); }); ``` You can also export an array of middlewares: ```ts routes/_middleware.ts import { define } from "../utils.ts"; const middleware1 = define.middleware(async (ctx) => { console.log("A"); return await ctx.next(); }); const middleware2 = define.middleware(async (ctx) => { console.log("B"); return await ctx.next(); }); export default [middleware1, middleware2]; ``` ================================================ FILE: docs/latest/concepts/routing.md ================================================ --- description: | File based routing is the simplest way to do routing in Fresh apps. Additionally custom patterns can be configured per route. --- Routing defines which middlewares and routes should respond to a particular request. ```ts main.ts const app = new App() .get("/", () => new Response("hello")) // Responds to: GET / .get("/other", () => new Response("other")) // Responds to: GET /other .post("/upload", () => new Response("upload")) // Responds to: POST /upload .get("/books/:id", (ctx) => { // Responds to: GET /books/my-book, /books/cool-book, etc const id = ctx.params.id; return new Response(`Book id: ${id}`); }) .get("/blog/:post/comments", () => { // Responds to: GET /blog/my-post/comments, /blog/hello/comments, etc const post = ctx.params.post; return new Response(`Blog post comments for post: ${post}`); }) .get("/foo/*", (ctx) => { // Responds to: GET /foo/bar, /foo/bar/baz, etc return new Response("foo"); }); ``` Fresh supports the full [`URLPattern`](https://developer.mozilla.org/en-US/docs/Web/API/URL_Pattern_API) syntax for setting pathnames. ================================================ FILE: docs/latest/concepts/static-files.md ================================================ --- description: | Fresh has built-in support for serving static files. This is useful for serving images, CSS, and other static assets. --- Static assets placed in the `static/` directory are served at the root of the webserver via the `staticFiles()` middleware. They are streamed directly from disk for optimal performance with [`ETag`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/ETag) headers. ```ts main.ts import { staticFiles } from "fresh"; const app = new App() .use(staticFiles()); ``` ## Caching headers By default, Fresh adds caching headers for the `src` and `srcset` attributes on `` and `` tags. ```tsx main.tsx // Caching headers will be automatically added app.get("/user", (ctx) => ctx.render()); ``` You can always opt out of this behaviour per tag, by adding the `data-fresh-disable-lock` attribute. ```tsx main.tsx // Opt-out of automatic caching headers app.get( "/user", (ctx) => ctx.render(), ); ``` ## Adding caching headers manually Use the `asset()` function to add caching headers manually. It will be served with a cache lifetime of one year. ```tsx routes/about.tsx import { asset } from "fresh/runtime"; export default function About() { // Adding caching headers manually return View brochure; } ``` ================================================ FILE: docs/latest/deployment/cloudflare-workers.md ================================================ --- description: "Deploy Fresh on Cloudflare Workers" --- Deploy Fresh to Cloudflare Workers by following these instructions: 1. Run `deno install --allow-scripts npm:@cloudflare/vite-plugin npm:wrangler` 2. Add the cloudflare plugin in your vite configuration file: ```diff vite.config.ts import { defineConfig } from "vite"; import { fresh } from "@fresh/plugin-vite"; + import { cloudflare } from "@cloudflare/vite-plugin"; export default defineConfig({ plugins: [ fresh(), + cloudflare(), ], }); ``` 3. Create a `server.js` file that serves as the cloudflare worker entry file: ```js import server from "./_fresh/server.js"; export default { fetch: server.fetch, }; ``` 4. Follow further instructions provided by the cloudflare vite plugin. Check out the [Cloudflare Documentation](https://developers.cloudflare.com/workers/vite-plugin/) for further information. > [info]: Make sure that you set the the correct entrypoint in your > `wrangler.jsonc` file. It should point to `"main": "./server.js"` ================================================ FILE: docs/latest/deployment/deno-compile.md ================================================ --- description: "Generate a self contained executable with deno compile." --- You can create a self-contained executable out of your app with the [`deno compile` command](https://docs.deno.com/runtime/reference/cli/compile/). It will include all assets and dependencies. This executable can run on any platform without requiring Deno to be installed. ```sh # Build your app first $ deno task build # Generate self-contained executable deno compile --output my-app --include _fresh -A _fresh/compiled-entry.js ``` The compiled entry supports two environment variables out of the box: - `PORT` to set the port number (`PORT=4000 my-app`) - `HOSTNAME` to set the host name number (`HOSTNAME=0.0.0.0 my-app`) ================================================ FILE: docs/latest/deployment/deno-deploy.md ================================================ --- description: "Deploy Fresh on Deno Deploy" --- The recommended way to deploy Fresh is by using [Deno Deploy](https://deno.com/deploy). It will automatically create branch previews for pull requests, collect request and HTTP metrics, as well has collect traces for you out of the box. 1. Log in to [Deno Deploy](https://deno.com/deploy) 1. Create a new app 1. Link your GitHub repository 1. Pick the "Fresh" preset if it's not already detected automatically Every time you merge into the `main` branch a new production deployment will be created. ================================================ FILE: docs/latest/deployment/docker.md ================================================ --- description: "Deploy Fresh with Docker" --- You can deploy Fresh to any platform that can run Docker containers. Docker is a tool to containerize projects and portably run them on any supported platform. When packaging your Fresh app for Docker, it is important that you set the `DENO_DEPLOYMENT_ID` environment variable in your container. This variable needs to be set to an opaque string ID that represents the version of your application that is currently being run. This could be a Git commit hash, or a hash of all files in your project. It is critical for the function of Fresh that this ID changes when _any_ file in your project changes - if it doesn't, incorrect caching **will** cause your project to not function correctly. Here is an example `Dockerfile` for a Fresh project: ```dockerfile Dockerfile FROM denoland/deno:latest ARG GIT_REVISION ENV DENO_DEPLOYMENT_ID=${GIT_REVISION} WORKDIR /app COPY . . RUN deno task build RUN deno cache _fresh/server.js EXPOSE 8000 CMD ["serve", "-A", "_fresh/server.js"] ``` To build your Docker image inside of a Git repository: ```sh Terminal $ docker build --build-arg GIT_REVISION=$(git rev-parse HEAD) -t my-fresh-app . ``` Then run your Docker container: ```sh Terminal $ docker run -t -i -p 80:8000 my-fresh-app ``` To deploy to a cloud provider, push it to a container registry and follow their documentation. - [Amazon Web Services](https://docs.aws.amazon.com/AmazonECS/latest/userguide/create-container-image.html#create-container-image-push-ecr) - [Google Cloud](https://cloud.google.com/container-registry/docs/pushing-and-pulling) ================================================ FILE: docs/latest/deployment/index.md ================================================ --- description: "Create a production build of your app" --- When shipping an app to production, we can run a build step that optimizes assets for consumption in the browser. This step can be invoked by running: ```sh Terminal deno task build # or deno run -A dev.ts build ``` Once completed, it will have created a `_fresh` folder in the project directory which contains the optimized assets. > [info]: The `_fresh` folder should not be committed to git. Exclude it via > `.gitignore`. > > ```gitignore .gitignore > # Ignore fresh build directory > _fresh/ > ``` ## Running a production build To run Fresh in production mode, run the `start` task: ```sh Terminal deno task start # or deno serve -A _fresh/server.js ``` Fresh will automatically pick up the optimized assets in the `_fresh` directory. ================================================ FILE: docs/latest/examples/active-links.md ================================================ --- description: | Style active links with ease in Fresh --- Fresh automatically enhances the accessibility of `` elements by adding the aria-current attribute when rendering links that match the current URL. This attribute is recognized by assistive technologies and clearly indicates the current page within a set of pages. - `aria-current="page"` - Added to links with an exact path match, enhancing accessibility by indicating the current page to assistive technologies. As we aim to improve accessibility, we encourage the use of aria-current for styling current links where applicable. ## Styling with CSS The aria-current attribute is easily styled with CSS using attribute selectors, providing a native way to visually differentiate the active link. ```css static/styles.css /* Give links pointing to the current page a green color */ a[aria-current="page"] { color: green; } /* Color all ancestor links of the current page */ a[aria-current="true"] { color: peachpuff; } ``` ## Tailwindcss In Tailwindcss or similar CSS frameworks, you can apply styles to elements with the `aria-current` attribute using bracket notation in your class definitions. For Tailwindcss, use the syntax: ```tsx components/Menu.tsx function Menu() { return ( Link to some page ); } ``` ================================================ FILE: docs/latest/examples/daisyui.md ================================================ --- title: Install daisyUI for Deno Fresh desc: How to install Tailwind CSS and daisyUI in a Deno Fresh project --- [daisyUI](https://daisyui.com/) is a component library for [Tailwind CSS](https://tailwindcss.com/) that provides semantic class names for common UI components like buttons, cards, and modals. It makes building beautiful interfaces faster while maintaining full Tailwind CSS compatibility. ## Installation To get started with daisyUI, make sure you have Tailwind CSS enabled in your Fresh project, then install daisyUI and update your configuration. 1. Run `deno i -D npm:daisyui@latest` to install daisyUI 2. Add daisyUI configuration in `./assets/styles.css`: ```diff assets/styles.css @import "tailwindcss"; + @plugin "daisyui"; ``` Now you're ready to use daisyUI. ## Using daisyUI Components Create a button component in the `components` directory, using daisyUI's style classes for reference. ```tsx components/Button.tsx import type { ComponentChildren } from "preact"; export interface ButtonProps { id?: string; onClick?: () => void; children?: ComponentChildren; disabled?: boolean; } export function Button(props: ButtonProps) { return (

{props.count}

); }`; await writeFile("islands/Counter.tsx", ISLANDS_COUNTER_TSX); const DEV_TS = `#!/usr/bin/env -S deno run -A --watch=static/,routes/ ${useTailwind ? `import { tailwind } from "@fresh/plugin-tailwind";\n` : ""} import { Builder } from "fresh/dev"; const builder = new Builder(); ${useTailwind ? "tailwind(builder);" : ""} if (Deno.args.includes("build")) { await builder.build(); } else { await builder.listen(() => import("./main.ts")); }`; if (!useVite) { await writeFile("dev.ts", DEV_TS); } const denoJson = { nodeModulesDir: "manual", tasks: { check: "deno fmt --check . && deno lint . && deno check", dev: "deno run -A --watch=static/,routes/ dev.ts", build: "deno run -A dev.ts build", start: "deno serve -A _fresh/server.js", update: "deno run -A -r jsr:@fresh/update .", }, lint: { rules: { tags: ["fresh", "recommended"], }, }, exclude: ["**/_fresh/*"], imports: { "@/": "./", "fresh": `jsr:@fresh/core@^${freshVersion}`, "preact": `npm:preact@^${PREACT_VERSION}`, "@preact/signals": `npm:@preact/signals@^${PREACT_SIGNALS_VERSION}`, } as Record, compilerOptions: { lib: ["dom", "dom.asynciterable", "dom.iterable", "deno.ns"], jsx: "precompile", jsxImportSource: "preact", jsxPrecompileSkipElements: [ "a", "img", "source", "body", "html", "head", "title", "meta", "script", "link", "style", "base", "noscript", "template", ], } as Record, }; if (useVite) { denoJson.compilerOptions.types = ["vite/client"]; denoJson.tasks.dev = "vite"; denoJson.tasks.build = "vite build"; const vitePluginVersion = await getLatestVersion( "@fresh/plugin-vite", FRESH_VITE_PLUGIN, ); denoJson.imports["@fresh/plugin-vite"] = `jsr:@fresh/plugin-vite@^${vitePluginVersion}`; denoJson.imports["vite"] = "npm:vite@^7.1.3"; if (useTailwind) { denoJson.imports["tailwindcss"] = `npm:tailwindcss@^${TAILWINDCSS_VERSION}`; denoJson.imports["@tailwindcss/vite"] = `npm:@tailwindcss/vite@^4.1.12`; } } else if (useTailwind) { denoJson.imports["tailwindcss"] = `npm:tailwindcss@^${TAILWINDCSS_VERSION}`; denoJson.imports["@fresh/plugin-tailwind"] = `jsr:@fresh/plugin-tailwind@^${FRESH_TAILWIND_VERSION}`; denoJson.imports["@tailwindcss/postcss"] = `npm:@tailwindcss/postcss@^${TAILWINDCSS_POSTCSS_VERSION}`; denoJson.imports["postcss"] = `npm:postcss@^${POSTCSS_VERSION}`; } await writeFile("deno.json", denoJson); if (useVite) { let viteConfig = `import { defineConfig } from "vite"; import { fresh } from "@fresh/plugin-vite";\n`; if (useTailwind) { viteConfig += `import tailwindcss from "@tailwindcss/vite";\n`; } viteConfig += `\nexport default defineConfig({ plugins: [fresh()${useTailwind ? ", tailwindcss()" : ""}], });`; await writeFile("vite.config.ts", viteConfig); } const README_MD = `# Fresh project Your new Fresh project is ready to go. You can follow the Fresh "Getting Started" guide here: https://fresh.deno.dev/docs/getting-started ### Usage Make sure to install Deno: https://docs.deno.com/runtime/getting_started/installation Then start the project in development mode: \`\`\` deno task dev \`\`\` This will watch the project directory and restart as necessary.`; await writeFile("README.md", README_MD); if (useVSCode) { const vscodeSettings = { "deno.enable": true, "deno.lint": true, "editor.defaultFormatter": "denoland.vscode-deno", "[typescriptreact]": { "editor.defaultFormatter": "denoland.vscode-deno", }, "[typescript]": { "editor.defaultFormatter": "denoland.vscode-deno", }, "[javascriptreact]": { "editor.defaultFormatter": "denoland.vscode-deno", }, "[javascript]": { "editor.defaultFormatter": "denoland.vscode-deno", }, "files.associations": useTailwind ? { "*.css": "tailwindcss", } : undefined, }; await writeFile(".vscode/settings.json", vscodeSettings); const recommendations = ["denoland.vscode-deno"]; if (useTailwind) recommendations.push("bradlc.vscode-tailwindcss"); await writeFile(".vscode/extensions.json", { recommendations }); } if (!flags.skipInstall) { console.log("Installing dependencies..."); await new Deno.Command(Deno.execPath(), { cwd: unresolvedDirectory, args: ["install"], }).output(); console.log("Installing dependencies...%cdone!", "color: green"); } // Specifically print unresolvedDirectory, rather than resolvedDirectory in order to // not leak personal info (e.g. `/Users/MyName`) console.log( "\n%cProject initialized!\n", "color: green; font-weight: bold", ); if (unresolvedDirectory !== ".") { console.log( `Enter your project directory using %ccd ${unresolvedDirectory}%c.`, "color: cyan", "", ); } console.log( "Run %cdeno task dev%c to start the project. %cCTRL-C%c to stop.", "color: cyan", "", "color: cyan", "", ); console.log(); console.log( "Stuck? Join our Discord %chttps://discord.gg/deno", "color: cyan", "", ); console.log(); console.log( "%cHappy hacking! 🦕", "color: gray", ); } async function writeProjectFile( projectDir: string, pathname: string, content: | string | Uint8Array | ReadableStream | Record, ) { const filePath = path.join( projectDir, ...pathname.split("/").filter(Boolean), ); try { await Deno.mkdir( path.dirname(filePath), { recursive: true }, ); if (typeof content === "string") { let formatted = content; if (!content.endsWith("\n\n")) { formatted += "\n"; } await Deno.writeTextFile(filePath, formatted); } else if ( content instanceof Uint8Array || content instanceof ReadableStream ) { await Deno.writeFile(filePath, content); } else { await Deno.writeTextFile( filePath, JSON.stringify(content, null, 2) + "\n", ); } } catch (err) { if (!(err instanceof Deno.errors.AlreadyExists)) { throw err; } } } interface JsrMeta { scope: string; name: string; latest: string | null; versions: Record; } async function getLatestVersion( pkg: string, fallback: string, ): Promise { // deno-lint-ignore no-explicit-any if ((globalThis as any).INIT_TEST) { return fallback; } try { const res = await fetch(`https://jsr.io/${pkg}/meta.json`); const json = (await res.json()) as JsrMeta; if (json.latest !== null) { return json.latest; } const versions = Object.keys(json.versions); if (versions.length === 0) throw new Error("No versions"); versions.sort((a, b) => { const s1 = semver.parse(a); const s2 = semver.parse(b); return semver.compare(s1, s2); }); return versions.at(-1)!; } catch { console.log( `Could not fetch latest ${pkg} version. Falling back to: ${fallback}`, ); return fallback; } } ================================================ FILE: packages/init/src/init_test.ts ================================================ import { expect } from "@std/expect"; import { CONFIRM_TAILWIND_MESSAGE, CONFIRM_VITE_MESSAGE, CONFIRM_VSCODE_MESSAGE, HELP_TEXT, initProject, } from "./init.ts"; import * as path from "@std/path"; import { getStdOutput, withBrowser } from "../../fresh/tests/test_utils.tsx"; import { waitForText } from "../../fresh/tests/test_utils.tsx"; import { withChildProcessServer } from "../../fresh/tests/test_utils.tsx"; import { withTmpDir as withTmpDirBase } from "../../fresh/src/test_utils.ts"; import { stub } from "@std/testing/mock"; // deno-lint-ignore no-explicit-any (globalThis as any).INIT_TEST = true; const testInitProject: typeof initProject = async (cwd, input, flags = {}) => { return await initProject(cwd, input, { ...flags, skipInstall: true }); }; function stubPrompt(result: string) { return stub(globalThis, "prompt", () => result); } function stubConfirm(steps: Record = {}) { return stub( globalThis, "confirm", (message) => message ? steps[message] : false, ); } function stubLogs() { return stub( console, "log", () => undefined, ); } function withTmpDir(): Promise<{ dir: string } & AsyncDisposable> { // Windows need temporary files in the repository root if (Deno.build.os === "windows") { const dir = path.join(import.meta.dirname!, "..", "..", "..", ".."); return withTmpDirBase({ dir, prefix: "tmp_" }); } return withTmpDirBase(); } async function patchProject(dir: string): Promise { const jsonPath = path.join(dir, "deno.json"); const json = JSON.parse(await Deno.readTextFile(jsonPath)); const linkedDir = path.join(dir, "_linked"); try { await Deno.mkdir(linkedDir, { recursive: true }); } catch (err) { if (!(err instanceof Deno.errors.AlreadyExists)) { throw err; } } const ignore = (entry: Deno.DirEntry, _full: string) => { return /^(\.|tmp_)/.test(entry.name) || /_test\.[tj]sx?$/.test(entry.name) || entry.name === "tests" || entry.name === "test"; }; await copyRecursive( path.join(import.meta.dirname!, "..", "..", "fresh"), path.join(linkedDir, "fresh"), ignore, ); await copyRecursive( path.join(import.meta.dirname!, "..", "..", "build-id"), path.join(linkedDir, "build-id"), ignore, ); json.workspace = ["./_linked/*"]; json.exclude = [...json.exclude ?? [], "**/_linked/*"]; // assert with this stricter rule, before adding it to initialized projects json.lint.rules.include = ["verbatim-module-syntax"]; await Deno.writeTextFile(jsonPath, JSON.stringify(json, null, 2) + "\n"); const installProc = await new Deno.Command(Deno.execPath(), { cwd: dir, args: ["install"], }).output(); if (installProc.code !== 0) { const { stderr, stdout } = getStdOutput(installProc); // deno-lint-ignore no-console console.log(stderr); // deno-lint-ignore no-console console.log(stdout); } expect(installProc.code).toEqual(0); } async function copyRecursive( from: string, to: string, ignore?: (entry: Deno.DirEntry, full: string) => boolean, ): Promise { for await (const entry of Deno.readDir(from)) { const source = path.join(from, entry.name); const target = path.join(to, entry.name); if (ignore && ignore(entry, source)) { continue; } if (entry.isFile) { try { await Deno.mkdir(to, { recursive: true }); } catch (err) { if (!(err instanceof Deno.errors.AlreadyExists)) { throw err; } } await Deno.copyFile(source, target); } else if (entry.isDirectory) { await copyRecursive(source, target, ignore); } } } async function expectProjectFile(dir: string, pathname: string) { const filePath = path.join(dir, ...pathname.split("/").filter(Boolean)); const stat = await Deno.stat(filePath); if (!stat.isFile) { throw new Error(`Not a project file: ${filePath}`); } } async function expectNotProjectFile(dir: string, pathname: string) { const filePath = path.join(dir, ...pathname.split("/").filter(Boolean)); try { const stat = await Deno.stat(filePath); if (stat.isFile) { throw new Error(`A project file but expected it not to be: ${filePath}`); } } catch (err) { if (!(err instanceof Deno.errors.NotFound)) { throw err; } } } async function readProjectFile(dir: string, pathname: string): Promise { const filePath = path.join(dir, ...pathname.split("/").filter(Boolean)); const content = await Deno.readTextFile(filePath); return content; } Deno.test("init - show help", async () => { using logs = stubLogs(); await testInitProject("", [], { help: true }); const args = logs.calls.flatMap((c) => c.args); const out = args.join("\n"); await testInitProject("", [], { h: true }); expect(out).toBe(HELP_TEXT); expect(args).toEqual(logs.calls.flatMap((c) => c.args).slice(args.length)); }); Deno.test("init - new project", async () => { await using tmp = await withTmpDir(); using _promptStub = stubPrompt("fresh-init"); using _confirmStub = stubConfirm(); await testInitProject(tmp.dir, [], { builder: true }); }); Deno.test("init - create project dir", async () => { await using tmp = await withTmpDir(); const dir = tmp.dir; using _promptStub = stubPrompt("fresh-init"); using _confirmStub = stubConfirm(); await testInitProject(dir, [], { builder: true }); const root = path.join(dir, "fresh-init"); await expectProjectFile(root, "deno.json"); await expectProjectFile(root, "main.ts"); await expectProjectFile(root, "dev.ts"); await expectProjectFile(root, ".gitignore"); await expectProjectFile(root, "static/styles.css"); }); Deno.test("init - with tailwind", async () => { await using tmp = await withTmpDir(); const dir = tmp.dir; using _promptStub = stubPrompt("."); using _confirmStub = stubConfirm({ [CONFIRM_TAILWIND_MESSAGE]: true, }); await testInitProject(dir, [], { builder: true }); const css = await readProjectFile(dir, "static/styles.css"); expect(css).toMatch(/@import "tailwindcss"/); const main = await readProjectFile(dir, "main.ts"); const dev = await readProjectFile(dir, "dev.ts"); expect(main).not.toMatch(/tailwind/); expect(dev).toMatch(/tailwind/); }); Deno.test("init - with vscode", async () => { await using tmp = await withTmpDir(); const dir = tmp.dir; using _promptStub = stubPrompt("."); using _confirmStub = stubConfirm({ [CONFIRM_VSCODE_MESSAGE]: true, }); await testInitProject(dir, [], { builder: true }); await expectProjectFile(dir, ".vscode/settings.json"); await expectProjectFile(dir, ".vscode/extensions.json"); }); Deno.test({ name: "init - fmt, lint, and type check project", // Ignore this test on canary due to different formatting // behaviours when the formatter changes. ignore: Deno.version.deno.includes("+"), fn: async () => { await using tmp = await withTmpDir(); const dir = tmp.dir; using _promptStub = stubPrompt("."); using _confirmStub = stubConfirm(); await testInitProject(dir, [], { builder: true }); await expectProjectFile(dir, "main.ts"); await expectProjectFile(dir, "dev.ts"); await patchProject(dir); const check = await new Deno.Command(Deno.execPath(), { args: ["task", "check"], cwd: dir, stderr: "inherit", stdout: "inherit", }).output(); expect(check.code).toEqual(0); }, }); Deno.test( "init with tailwind - fmt, lint, and type check project", async () => { await using tmp = await withTmpDir(); const dir = tmp.dir; using _promptStub = stubPrompt("."); using _confirmStub = stubConfirm({ [CONFIRM_TAILWIND_MESSAGE]: true, }); await testInitProject(dir, [], { builder: true }); await expectProjectFile(dir, "main.ts"); await expectProjectFile(dir, "dev.ts"); await patchProject(dir); const check = await new Deno.Command(Deno.execPath(), { args: ["task", "check"], cwd: dir, stderr: "inherit", stdout: "inherit", }).output(); expect(check.code).toEqual(0); }, ); Deno.test({ // TODO: For some reason this test is flaky in GitHub CI. It works when // testing locally on windows though. Not sure what's going on. ignore: Deno.build.os === "windows" && Deno.env.get("CI") !== undefined, name: "init - can start dev server", fn: async () => { await using tmp = await withTmpDir(); const dir = tmp.dir; using _promptStub = stubPrompt("."); using _confirmStub = stubConfirm(); await testInitProject(dir, [], { builder: true }); await expectProjectFile(dir, "main.ts"); await expectProjectFile(dir, "dev.ts"); await patchProject(dir); await withChildProcessServer( { cwd: dir, args: ["task", "dev"] }, async (address) => { await withBrowser(async (page) => { await page.goto(address); await page.locator("#decrement").click(); await waitForText(page, "button + p", "2"); }); }, ); }, }); Deno.test("init - can start built project", async () => { await using tmp = await withTmpDir(); const dir = tmp.dir; using _promptStub = stubPrompt("."); using _confirmStub = stubConfirm(); await testInitProject(dir, [], { builder: true }); await expectProjectFile(dir, "main.ts"); await expectProjectFile(dir, "dev.ts"); await patchProject(dir); // Build await new Deno.Command(Deno.execPath(), { args: ["task", "build"], stdin: "null", stdout: "piped", stderr: "piped", cwd: dir, }).output(); await withChildProcessServer( { cwd: dir, args: ["serve", "-A", "--port", "0", "_fresh/server.js"] }, async (address) => { await withBrowser(async (page) => { await page.goto(address); await page.locator("button").click(); await waitForText(page, "button + p", "2"); }); }, ); }); Deno.test("init - errors on missing build cache in prod", async () => { await using tmp = await withTmpDir(); const dir = tmp.dir; using _promptStub = stubPrompt("."); using _confirmStub = stubConfirm(); await testInitProject(dir, [], { builder: true }); await expectProjectFile(dir, "main.ts"); await expectProjectFile(dir, "dev.ts"); await patchProject(dir); const cp = await new Deno.Command(Deno.execPath(), { args: ["task", "start"], stdin: "null", stdout: "piped", stderr: "piped", cwd: dir, }).output(); const { stderr } = getStdOutput(cp); expect(cp.code).toEqual(1); expect(stderr).toMatch(/Module not found/); }); // There is a peerDependency issue with links Deno.test.ignore("init - vite dev server", async () => { await using tmp = await withTmpDir(); const dir = tmp.dir; using _promptStub = stubPrompt("."); using _confirmStub = stubConfirm(); await testInitProject(dir, [], {}); await expectProjectFile(dir, "vite.config.ts"); await expectNotProjectFile(dir, "dev.ts"); await patchProject(dir); await withChildProcessServer( { cwd: dir, args: ["task", "dev"] }, async (address) => { await withBrowser(async (page) => { await page.goto(address); await page.locator("#decrement").click(); await waitForText(page, "button + p", "2"); }); }, ); }); // There is a peerDependency issue with links Deno.test.ignore("init - vite build", async () => { await using tmp = await withTmpDir(); const dir = tmp.dir; using _promptStub = stubPrompt("."); using _confirmStub = stubConfirm(); await testInitProject(dir, [], {}); await expectProjectFile(dir, "vite.config.ts"); await patchProject(dir); // Build await new Deno.Command(Deno.execPath(), { args: ["task", "build"], stdin: "null", stdout: "piped", stderr: "piped", cwd: dir, }).output(); await withChildProcessServer( { cwd: dir, args: ["serve", "-A", "--port", "0", "_fresh/server.js"] }, async (address) => { await withBrowser(async (page) => { await page.goto(address); await page.locator("button").click(); await waitForText(page, "button + p", "2"); }); }, ); }); Deno.test("init - with vite", async () => { await using tmp = await withTmpDir(); const dir = tmp.dir; using _promptStub = stubPrompt("."); using _confirmStub = stubConfirm({ [CONFIRM_VITE_MESSAGE]: true, }); await testInitProject(dir, [], {}); await expectProjectFile(dir, "vite.config.ts"); await expectNotProjectFile(dir, "dev.ts"); }); ================================================ FILE: packages/init/src/mod.ts ================================================ import { parseArgs } from "@std/cli/parse-args"; import { initProject } from "./init.ts"; import { InitError } from "./init.ts"; const flags = parseArgs(Deno.args, { boolean: ["force", "tailwind", "vscode", "docker", "help", "builder"], default: { force: null, tailwind: null, vscode: null, docker: null, builder: null, }, alias: { help: "h", }, }); try { await initProject(Deno.cwd(), flags._, flags); } catch (err) { if (err instanceof InitError) { Deno.exit(1); } throw err; } ================================================ FILE: packages/plugin-tailwindcss/README.md ================================================ # Tailwind CSS plugin for Fresh A Tailwind CSS plugin to use in Fresh. ## Basic Usage ```ts // dev.ts import { Builder } from "fresh/dev"; import { app } from "./main.ts"; import { tailwind } from "@fresh/plugin-tailwind"; const builder = new Builder(); tailwind(builder, app); if (Deno.args.includes("build")) { builder.build(app); } else { builder.listen(app); } ``` ## Option Configuration ```ts // dev.ts import { Builder } from "fresh/dev"; import { app } from "./main.ts"; import { tailwind } from "@fresh/plugin-tailwind"; const builder = new Builder(); tailwind(builder, app, { // Exclude certain files from processing exclude: ["/admin/**", "*.temp.css"], // Force optimization (defaults to production mode) optimize: true, // Exclude base styles base: null, }); if (Deno.args.includes("build")) { builder.build(app); } else { builder.listen(app); } ``` To learn more about Fresh go to [https://fresh.deno.dev/](https://fresh.deno.dev/). ================================================ FILE: packages/plugin-tailwindcss/deno.json ================================================ { "name": "@fresh/plugin-tailwind", "version": "1.0.0", "license": "MIT", "exports": "./src/mod.ts" } ================================================ FILE: packages/plugin-tailwindcss/src/mod.ts ================================================ import type { Builder } from "fresh/dev"; import twPostcss from "@tailwindcss/postcss"; import postcss from "postcss"; import type { TailwindPluginOptions } from "./types.ts"; // Re-export types for public API export type { TailwindPluginOptions } from "./types.ts"; export function tailwind( builder: Builder, options: TailwindPluginOptions = {}, ): void { const { exclude, ...tailwindOptions } = options; const instance = postcss(twPostcss({ optimize: builder.config.mode === "production", ...tailwindOptions, })); builder.onTransformStaticFile( { pluginName: "tailwind", filter: /\.css$/, exclude }, async (args) => { const res = await instance.process(args.text, { from: args.path, }); return { content: res.content, map: res.map?.toString(), }; }, ); } ================================================ FILE: packages/plugin-tailwindcss/src/types.ts ================================================ import type { OnTransformOptions } from "fresh/dev"; // based on the original code, this type is used to define options for the Tailwind plugin type PluginOptions = { /** * Base CSS to be included. Set to null to exclude base styles. */ base?: string; /** * Enable or disable CSS optimization. Defaults to true if Fresh is in production mode and false otherwise. * Can be a boolean or an object with minify options. * @default app.config.mode === "production" */ optimize?: boolean | { minify?: boolean; }; }; export interface TailwindPluginOptions extends PluginOptions { /** Exclude paths or globs that should not be processed */ exclude?: OnTransformOptions["exclude"]; } ================================================ FILE: packages/plugin-tailwindcss-v3/README.md ================================================ # Tailwind CSS v3 plugin for Fresh A Tailwind CSS v3 plugin to use in Fresh. > [info]: If you want to use latest tailwindcss use `@fresh/plugin-tailwindcss` > instead. ## Basic Usage ```ts // dev.ts import { Builder } from "fresh/dev"; import { tailwind } from "@fresh/plugin-tailwind-v3"; const builder = new Builder(); tailwind(builder); if (Deno.args.includes("build")) { builder.build(); } else { builder.listen(() => import("./main.ts")); } ``` ## Option Configuration ```ts tailwind(builder, { // Exclude certain files from processing exclude: ["/admin/**", "*.temp.css"], // Force optimization (defaults to production mode) optimize: true, // Exclude base styles base: null, }); ``` To learn more about Fresh go to [https://fresh.deno.dev/](https://fresh.deno.dev/). ================================================ FILE: packages/plugin-tailwindcss-v3/deno.json ================================================ { "name": "@fresh/plugin-tailwind-v3", "version": "1.0.1", "license": "MIT", "exports": "./src/mod.ts", "imports": { "autoprefixer": "npm:autoprefixer@^10.4.21", "cssnano": "npm:cssnano@^6.1.2", "postcss": "npm:postcss@^8.5.6", "tailwindcss": "npm:tailwindcss@^3.4.17" } } ================================================ FILE: packages/plugin-tailwindcss-v3/src/mod.ts ================================================ import type { Builder, ResolvedBuildConfig } from "fresh/dev"; import tailwindCss, { type Config } from "tailwindcss"; import postcss from "postcss"; import cssnano from "cssnano"; import autoprefixer from "autoprefixer"; import * as path from "@std/path"; export interface AutoprefixerOptions { /** environment for `Browserslist` */ env?: string; /** should Autoprefixer use Visual Cascade, if CSS is uncompressed */ cascade?: boolean; /** should Autoprefixer add prefixes. */ add?: boolean; /** should Autoprefixer [remove outdated] prefixes */ remove?: boolean; /** should Autoprefixer add prefixes for @supports parameters. */ supports?: boolean; /** should Autoprefixer add prefixes for flexbox properties */ flexbox?: boolean | "no-2009"; /** should Autoprefixer add IE 10-11 prefixes for Grid Layout properties */ grid?: boolean | "autoplace" | "no-autoplace"; /** custom usage statistics for > 10% in my stats browsers query */ stats?: { [browser: string]: { [version: string]: number; }; }; /** * list of queries for target browsers. * Try to not use it. * The best practice is to use `.browserslistrc` config or `browserslist` key in `package.json` * to share target browsers with Babel, ESLint and Stylelint */ overrideBrowserslist?: string | string[]; /** do not raise error on unknown browser version in `Browserslist` config. */ ignoreUnknownVersions?: boolean; } export interface TailwindPluginOptions { autoprefixer?: AutoprefixerOptions; } export function tailwind( builder: Builder, options: TailwindPluginOptions = {}, ): void { const processor = initTailwind(builder.config, options); builder.onTransformStaticFile( { pluginName: "tailwind", filter: /\.css$/ }, async (args) => { const instance = await processor; const res = await instance.process(args.text, { from: args.path, }); return { content: res.content, map: res.map?.toString(), }; }, ); } const CONFIG_EXTENSIONS = ["ts", "js", "mjs"]; async function findTailwindConfigFile(directory: string): Promise { let dir = directory; while (true) { for (let i = 0; i < CONFIG_EXTENSIONS.length; i++) { const ext = CONFIG_EXTENSIONS[i]; const filePath = path.join(dir, `tailwind.config.${ext}`); try { const stat = await Deno.stat(filePath); if (stat.isFile) { return filePath; } } catch (err) { if (!(err instanceof Deno.errors.NotFound)) { throw err; } } } const parent = path.dirname(dir); if (parent === dir) { throw new Error( `Could not find a tailwind config file in the current directory or any parent directory.`, ); } dir = parent; } } async function initTailwind( config: ResolvedBuildConfig, options: TailwindPluginOptions, ): Promise { const root = path.dirname(config.staticDir); const configPath = await findTailwindConfigFile(root); const url = path.toFileUrl(configPath).href; const tailwindConfig = (await import(url)).default as Config; if (!Array.isArray(tailwindConfig.content)) { throw new Error(`Expected tailwind "content" option to be an array`); } // deno-lint-ignore no-explicit-any tailwindConfig.content = tailwindConfig.content.map((pattern: any) => { if (typeof pattern === "string") { const relative = path.relative(Deno.cwd(), path.dirname(configPath)); if (!relative.startsWith("..")) { return path.join(relative, pattern); } } return pattern; }); // PostCSS types cause deep recursion const plugins = [ // deno-lint-ignore no-explicit-any tailwindCss(tailwindConfig) as any, // deno-lint-ignore no-explicit-any autoprefixer(options.autoprefixer) as any, ]; if (config.mode === "production") { plugins.push(cssnano()); } return postcss(plugins); } ================================================ FILE: packages/plugin-vite/README.md ================================================ # Fresh vite plugin Vite plugin for [Fresh](https://fresh.deno.dev). ## Usage 1. Run `deno install jsr:@fresh/plugin-vite npm:vite` 2. Ensure that your `deno.json` has the `fresh` import mapping: ```json { "imports": { "fresh": "jsr:@fresh/core" } } ``` 3. Update your `vite.config.ts` file: ```diff import { defineConfig } from "vite"; + import { fresh } from "@fresh/plugin-vite"; export default defineConfig({ plugins: [ + fresh(), ], }); ``` 4. Update your deno tasks respectively: - Dev: `vite` - Build: `vite build` More information [on the Fresh documentation](https://fresh.deno.dev/docs/advanced/vite) . ================================================ FILE: packages/plugin-vite/demo/assets/style.css ================================================ @import "tailwindcss"; ================================================ FILE: packages/plugin-vite/demo/client.ts ================================================ import "./assets/style.css"; ================================================ FILE: packages/plugin-vite/demo/components/CssModuleNonIsland.tsx ================================================ // @ts-ignore upstream issue https://github.com/denoland/deno/issues/30560 import styles from "./CssModulesNonIsland.module.css"; export function CssModulesNonIsland() { return (

green text

); } ================================================ FILE: packages/plugin-vite/demo/components/CssModulesNonIsland.module.css ================================================ .root { color: green; } ================================================ FILE: packages/plugin-vite/demo/fixtures/commonjs_mod.cjs ================================================ exports.value = "ok"; ================================================ FILE: packages/plugin-vite/demo/fixtures/maxmind.cjs ================================================ "use strict"; // deno-lint-ignore no-var var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const assert_1 = __importDefault(require("assert")); (0, assert_1.default)(true); ================================================ FILE: packages/plugin-vite/demo/islands/Bar.tsx ================================================ import { useState } from "preact/hooks"; export function Bar() { const [count, set] = useState(0); return (

island asdf

); } ================================================ FILE: packages/plugin-vite/demo/islands/CssModules.module.css ================================================ .root { color: red; } ================================================ FILE: packages/plugin-vite/demo/islands/CssModules.tsx ================================================ import { CssModulesNonIsland } from "../components/CssModuleNonIsland.tsx"; // @ts-ignore upstream issue https://github.com/denoland/deno/issues/30560 import styles from "./CssModules.module.css"; import { CssModulesOther } from "./CssModulesOther.tsx"; export function CssModules() { return (

red text

); } ================================================ FILE: packages/plugin-vite/demo/islands/CssModulesOther.module.css ================================================ .root { color: blue; } ================================================ FILE: packages/plugin-vite/demo/islands/CssModulesOther.tsx ================================================ // @ts-ignore upstream issue https://github.com/denoland/deno/issues/30560 import styles from "./CssModulesOther.module.css"; export function CssModulesOther() { return (

blue text

); } ================================================ FILE: packages/plugin-vite/demo/islands/Foo.tsx ================================================ import { useSignal } from "@preact/signals"; export function Foo() { const count = useSignal(0); return (

island

); } ================================================ FILE: packages/plugin-vite/demo/islands/IslandNestedInner.tsx ================================================ import { useEffect, useState } from "preact/hooks"; export function IslandNestedInner() { const [ready, set] = useState(false); useEffect(() => { set(true); }, []); return (

Inner

); } ================================================ FILE: packages/plugin-vite/demo/islands/IslandNestedOuter.tsx ================================================ import { useEffect, useState } from "preact/hooks"; import { IslandNestedInner } from "./IslandNestedInner.tsx"; export function IslandNestedOuter() { const [ready, set] = useState(false); useEffect(() => { set(true); }, []); return (

Outer

); } ================================================ FILE: packages/plugin-vite/demo/islands/tests/CounterHooks.tsx ================================================ import { useState } from "preact/hooks"; export function CounterHooks() { const [count, set] = useState(0); return (
); } ================================================ FILE: packages/plugin-vite/demo/islands/tests/EnvIsland.tsx ================================================ import { useEffect, useState } from "preact/hooks"; export function EnvIsland() { const [ready, setReady] = useState(false); useEffect(() => { setReady(true); }, []); const deno = Deno.env.get("FRESH_PUBLIC_FOO"); // deno-lint-ignore no-process-global const nodeEnv = process.env.FRESH_PUBLIC_FOO; return (
{JSON.stringify({ deno,nodeEnv},null,2)}
); } ================================================ FILE: packages/plugin-vite/demo/islands/tests/IslandAssets.tsx ================================================ import logo from "../../assets/deno-logo.png"; export function IslandAssets() { return ; } ================================================ FILE: packages/plugin-vite/demo/islands/tests/Mime.tsx ================================================ import mime from "mime-db"; import { useEffect, useState } from "preact/hooks"; export function MimeIsland() { const [ready, setReady] = useState(false); useEffect(() => { // deno-lint-ignore no-console console.log(mime); setReady(true); }, []); return

mime

; } ================================================ FILE: packages/plugin-vite/demo/islands/tests/Ready.tsx ================================================ import { useEffect, useState } from "preact/hooks"; export function ReadyIsland() { const [ready, set] = useState(false); useEffect(() => { set(true); }, []); return (
{ready ? "ready" : "waiting..."}
); } ================================================ FILE: packages/plugin-vite/demo/main.ts ================================================ import { App, staticFiles, trailingSlashes } from "fresh"; export const app = new App() .use(staticFiles()) .use(trailingSlashes("never")) .get("/", () => new Response("it works")) .fsRoutes(); ================================================ FILE: packages/plugin-vite/demo/routes/_error.tsx ================================================ import { define } from "../utils.ts"; export default define.page((props) => { if (props.error instanceof Error) { return
{String(props.error?.stack)}
; } return
{String(props.error)}
; }); ================================================ FILE: packages/plugin-vite/demo/routes/about.tsx ================================================ import { Bar } from "../islands/Bar.tsx"; import { Foo } from "../islands/Foo.tsx"; export const handler = () => { return { data: {} }; }; export default function About() { return (

Hello from Vite

This page is the prod Fresh build and served by vite

); } ================================================ FILE: packages/plugin-vite/demo/routes/index.tsx ================================================ export default function Index() { return

index page2

; } ================================================ FILE: packages/plugin-vite/demo/routes/tests/CssRoute.module.css ================================================ .root { color: peachpuff; } ================================================ FILE: packages/plugin-vite/demo/routes/tests/api/[id].tsx ================================================ export const handler = () => new Response("ok"); ================================================ FILE: packages/plugin-vite/demo/routes/tests/assets.tsx ================================================ import logo from "../../assets/deno-logo.png"; export default function Page() { return ; } ================================================ FILE: packages/plugin-vite/demo/routes/tests/build_id.tsx ================================================ import { BUILD_ID } from "@fresh/build-id"; export const handler = () => new Response(BUILD_ID); ================================================ FILE: packages/plugin-vite/demo/routes/tests/commonjs.tsx ================================================ import { value } from "../../fixtures/commonjs_mod.cjs"; export default function Page() { return

{value}

; } ================================================ FILE: packages/plugin-vite/demo/routes/tests/css.tsx ================================================ // @ts-ignore upstream issue https://github.com/denoland/deno/issues/30560 import "./css_styles.css"; export default function Page() { return (

red text

); } ================================================ FILE: packages/plugin-vite/demo/routes/tests/css_modules.tsx ================================================ import { CssModules } from "../../islands/CssModules.tsx"; // @ts-ignore upstream issue https://github.com/denoland/deno/issues/30560 import styles from "./CssRoute.module.css"; export default function Page() { return (

peachpuff text

); } ================================================ FILE: packages/plugin-vite/demo/routes/tests/css_styles.css ================================================ h1 { color: red; } ================================================ FILE: packages/plugin-vite/demo/routes/tests/dep_json.tsx ================================================ import { value } from "@marvinh-test/import-json"; export const handler = () => Response.json(value); ================================================ FILE: packages/plugin-vite/demo/routes/tests/env.tsx ================================================ import { EnvIsland } from "../../islands/tests/EnvIsland.tsx"; export default function Page() { return ; } ================================================ FILE: packages/plugin-vite/demo/routes/tests/env_files.tsx ================================================ export const handler = () => { const json = { MY_ENV: Deno.env.get("MY_ENV"), VITE_MY_ENV: Deno.env.get("VITE_MY_ENV"), MY_LOCAL_ENV: Deno.env.get("MY_LOCAL_ENV"), VITE_MY_LOCAL_ENV: Deno.env.get("VITE_MY_LOCAL_ENV"), }; return Response.json(json); }; ================================================ FILE: packages/plugin-vite/demo/routes/tests/feed.tsx ================================================ import * as feed from "feed"; export default function Page() { // deno-lint-ignore no-console console.log(feed); return

feed

; } ================================================ FILE: packages/plugin-vite/demo/routes/tests/ioredis.tsx ================================================ import * as ioredis from "ioredis"; export default function Page() { // deno-lint-ignore no-console console.log(ioredis); return

ioredis

; } ================================================ FILE: packages/plugin-vite/demo/routes/tests/island_assets.tsx ================================================ import { IslandAssets } from "../../islands/tests/IslandAssets.tsx"; export default function Page() { return ; } ================================================ FILE: packages/plugin-vite/demo/routes/tests/island_hooks.tsx ================================================ import { CounterHooks } from "../../islands/tests/CounterHooks.tsx"; export default function Page() { return ; } ================================================ FILE: packages/plugin-vite/demo/routes/tests/island_nested.tsx ================================================ import { IslandNestedOuter } from "../../islands/IslandNestedOuter.tsx"; export default function Page() { return (
); } ================================================ FILE: packages/plugin-vite/demo/routes/tests/it_works.tsx ================================================ export default function Page() { return

it works

; } ================================================ FILE: packages/plugin-vite/demo/routes/tests/jsx_namespace.tsx ================================================ export default function Page() { return ( ); } ================================================ FILE: packages/plugin-vite/demo/routes/tests/maxmind.tsx ================================================ import * as maxmind from "../../fixtures/maxmind.cjs"; export default function Page() { // deno-lint-ignore no-console console.log(maxmind); return

maxmind

; } ================================================ FILE: packages/plugin-vite/demo/routes/tests/middlewares/_middleware.ts ================================================ import type { Middleware } from "@fresh/core"; const middleware1: Middleware<{ text: string }> = async (ctx) => { ctx.state.text = "A"; return await ctx.next(); }; const middleware2: Middleware<{ text: string }> = async (ctx) => { ctx.state.text += "B"; return await ctx.next(); }; export default [middleware1, middleware2]; ================================================ FILE: packages/plugin-vite/demo/routes/tests/middlewares/index.tsx ================================================ import type { HandlerFn } from "@fresh/core"; export const handler: HandlerFn = (ctx) => new Response(ctx.state.text); ================================================ FILE: packages/plugin-vite/demo/routes/tests/mime.tsx ================================================ import { MimeIsland } from "../../islands/tests/Mime.tsx"; export default function Page() { return ; } ================================================ FILE: packages/plugin-vite/demo/routes/tests/partial.tsx ================================================ import { Partial } from "@fresh/core/runtime"; import { ReadyIsland } from "../../islands/tests/Ready.tsx"; export default function Page() { return (

Partial

click me
); } ================================================ FILE: packages/plugin-vite/demo/routes/tests/partial_insert.tsx ================================================ import { Partial } from "@fresh/core/runtime"; import { CounterHooks } from "../../islands/tests/CounterHooks.tsx"; export default function Page() { return ( ); } ================================================ FILE: packages/plugin-vite/demo/routes/tests/pg.tsx ================================================ import * as pg from "pg"; export default function Page() { // deno-lint-ignore no-console console.log(pg); return

pg

; } ================================================ FILE: packages/plugin-vite/demo/routes/tests/qs.tsx ================================================ import * as qs from "qs"; export default function Page() { // deno-lint-ignore no-console console.log(qs); return

qs

; } ================================================ FILE: packages/plugin-vite/demo/routes/tests/radix.tsx ================================================ import "@radix-ui/themes/styles.css"; import { Button, Theme } from "@radix-ui/themes"; export default function Page() { return ( ); } ================================================ FILE: packages/plugin-vite/demo/routes/tests/redis.tsx ================================================ import * as redis from "redis"; export default function Page() { // deno-lint-ignore no-console console.log(redis); return

redis

; } ================================================ FILE: packages/plugin-vite/demo/routes/tests/remote_island.tsx ================================================ import { RemoteIsland } from "@marvinh-test/fresh-island"; export default function Page() { return ; } ================================================ FILE: packages/plugin-vite/demo/routes/tests/stripe.tsx ================================================ import * as stripe from "stripe"; export default function Page() { // deno-lint-ignore no-console console.log(stripe); return

stripe

; } ================================================ FILE: packages/plugin-vite/demo/routes/tests/supabase_pg.tsx ================================================ import * as supabase from "@supabase/postgrest-js"; export default function Page() { // deno-lint-ignore no-console console.log(supabase); return

supabase

; } ================================================ FILE: packages/plugin-vite/demo/routes/tests/tailwind.tsx ================================================ export default function Page() { return

this should be red

; } ================================================ FILE: packages/plugin-vite/demo/routes/tests/throw.tsx ================================================ import { define } from "../../utils.ts"; export const handler = define.handlers({ GET() { throw new Error("FAIL"); }, }); ================================================ FILE: packages/plugin-vite/demo/static/foo.txt ================================================ foo ================================================ FILE: packages/plugin-vite/demo/static/test_static/foo/index.html ================================================ Document

ok

================================================ FILE: packages/plugin-vite/demo/static/test_static/foo.txt ================================================ it works ================================================ FILE: packages/plugin-vite/demo/utils.ts ================================================ import { createDefine } from "@fresh/core"; // deno-lint-ignore no-explicit-any export const define = createDefine(); ================================================ FILE: packages/plugin-vite/demo/vite.config.ts ================================================ import { defineConfig } from "vite"; import { fresh } from "@fresh/plugin-vite"; import inspect from "vite-plugin-inspect"; import tailwind from "@tailwindcss/vite"; export default defineConfig({ plugins: [ inspect(), fresh({ islandSpecifiers: ["@marvinh-test/fresh-island"], }), tailwind(), ], future: "warn", }); ================================================ FILE: packages/plugin-vite/deno.json ================================================ { "name": "@fresh/plugin-vite", "version": "1.0.8", "license": "MIT", "exports": { ".": "./src/mod.ts", "./client": "./src/client.ts" }, "publish": { "include": ["./src", "README.md"] }, "compilerOptions": { "jsx": "precompile", "jsxImportSource": "preact", "types": ["vite/client"] }, "tasks": { "demo": "vite demo", "debug": "deno run -A --inspect-brk npm:vite demo", "demo:build": "vite build demo", "demo:start": "cd demo && deno serve -A _fresh/server.js" }, "imports": { "@babel/core": "npm:@babel/core@^7.28.0", "@babel/preset-react": "npm:@babel/preset-react@^7.27.1", "@deno/loader": "jsr:@deno/loader@^0.3.10", "@marvinh-test/import-json": "jsr:@marvinh-test/import-json@^0.0.1", "@remix-run/node-fetch-server": "npm:@remix-run/node-fetch-server@^0.12.0", "@prefresh/vite": "npm:@prefresh/vite@^2.4.8", "@radix-ui/themes": "npm:@radix-ui/themes@^3.2.1", "@tailwindcss/vite": "npm:@tailwindcss/vite@^4.1.12", "@types/babel__core": "npm:@types/babel__core@^7.20.5", "@types/node": "npm:@types/node@^24.1.0", "@types/qs": "npm:@types/qs@^6.14.0", "feed": "npm:feed@^5.1.0", "fresh": "jsr:@fresh/core@^2.0.0", "ioredis": "npm:ioredis@^5.7.0", "mime-db": "npm:mime-db@^1.54.0", "pg": "npm:pg@^8.16.3", "preact": "npm:preact@^10.26.9", "qs": "npm:qs@^6.14.0", "rollup-plugin-visualizer": "npm:rollup-plugin-visualizer@^6.0.3", "stripe": "npm:stripe@^19.1.0", "vite": "npm:vite@^7.1.4", "vite-plugin-inspect": "npm:vite-plugin-inspect@^11.3.2" } } ================================================ FILE: packages/plugin-vite/src/client.ts ================================================ /** * Used internally by the `@fresh/plugin-vite` to add HMR support * for Fresh running in the browser. * * **Do not** import this module directly. * * @module * @private */ import type { UpdatePayload } from "vite"; import { hashCode } from "./shared.ts"; if (import.meta.hot) { import.meta.hot.on("fresh:reload", () => { window.location.reload(); }); import.meta.hot.on("vite:beforeUpdate", (module: UpdatePayload) => { module.updates.forEach((update) => { const moduleStyle = document.querySelector( `[vite-module-id="${hashCode(update.acceptedPath)}"]`, ); if (moduleStyle) { moduleStyle.remove(); } }); }); } ================================================ FILE: packages/plugin-vite/src/mod.ts ================================================ import type { Plugin } from "vite"; import { type FreshViteConfig, pathWithRoot, type ResolvedFreshViteConfig, } from "./utils.ts"; import { deno } from "./plugins/deno.ts"; import prefresh from "@prefresh/vite"; import { serverEntryPlugin } from "./plugins/server_entry.ts"; import { clientEntryPlugin } from "./plugins/client_entry.ts"; import { devServer } from "./plugins/dev_server.ts"; import { buildIdPlugin } from "./plugins/build_id.ts"; import { clientSnapshot } from "./plugins/client_snapshot.ts"; import { serverSnapshot } from "./plugins/server_snapshot.ts"; import { patches } from "./plugins/patches.ts"; import process from "node:process"; import { specToName, TEST_FILE_PATTERN, UniqueNamer, UPDATE_INTERVAL, updateCheck, } from "@fresh/core/internal-dev"; import { checkImports } from "./plugins/verify_imports.ts"; import { isBuiltin } from "node:module"; import { load as stdLoadEnv } from "@std/dotenv"; import path from "node:path"; export type { FreshViteConfig }; export type { ImportCheck, ImportCheckDiagnostic, } from "./plugins/verify_imports.ts"; /** * Fresh framework support for Vite. * * This plugin uses the Environments feature of Vite to build * both the server and client code for Fresh applications. * * @param config Fresh config options * @returns Vite plugin with Fresh support * * @example Basic usage * ```ts vite.config.ts * import { defineConfig } from "vite"; * import { fresh } from "@fresh/plugin-vite"; * * export default defineConfig({ * plugins: [ * fresh({ serverEntry: "server.ts" }) * ], * }); * ``` */ export function fresh(config?: FreshViteConfig): Plugin[] { const fConfig: ResolvedFreshViteConfig = { serverEntry: config?.serverEntry ?? "main.ts", clientEntry: config?.clientEntry ?? "client.ts", islandsDir: config?.islandsDir ?? "islands", routeDir: config?.routeDir ?? "routes", ignore: config?.ignore ?? [TEST_FILE_PATTERN], islandSpecifiers: new Map(), namer: new UniqueNamer(), checkImports: config?.checkImports ?? [], }; fConfig.checkImports.push((id, env) => { if (env === "client") { if (isBuiltin(id)) { return { type: "error", message: "Node built-in modules cannot be imported in the browser.", description: "This is an error in your application code or in one of its dependencies.", }; } } }); let isDev = false; const plugins: Plugin[] = [ { name: "fresh", sharedDuringBuild: true, config(config, env) { isDev = env.command === "serve"; return { esbuild: { jsx: "automatic", jsxImportSource: "preact", jsxDev: env.command === "serve", }, resolve: { alias: { "react-dom/test-utils": "preact/test-utils", "react-dom": "preact/compat", react: "preact/compat", }, // Disallow externals, because it leads to duplicate // modules with `preact` vs `npm:preact@*` in the server // environment. noExternal: true, }, optimizeDeps: { // Optimize deps somehow leads to duplicate modules or them // being placed in the wrong chunks... noDiscovery: true, }, publicDir: pathWithRoot("static", config.root), builder: { async buildApp(builder) { // Build client env first const clientEnv = builder.environments.client; if (clientEnv !== undefined) { await builder.build(clientEnv); } await Promise.all( Object.values(builder.environments).filter((env) => env !== clientEnv ).map((env) => builder.build(env)), ); }, }, environments: { client: { build: { copyPublicDir: false, manifest: true, outDir: config.environments?.client?.build?.outDir ?? (config.build?.outDir ? config.build.outDir + "/client" : null) ?? "_fresh/client", rollupOptions: { preserveEntrySignatures: "strict", input: { "client-entry": "fresh:client-entry", }, }, }, }, ssr: { build: { manifest: true, emitAssets: true, copyPublicDir: false, outDir: config.environments?.ssr?.build?.outDir ?? (config.build?.outDir ? config.build.outDir + "/server" : null) ?? "_fresh/server", rollupOptions: { onwarn(warning, handler) { // Ignore "use client"; warnings if (warning.code === "MODULE_LEVEL_DIRECTIVE") { return; } // Ignore optional export errors if ( warning.code === "MISSING_EXPORT" && warning.id?.startsWith("\0fresh-route::") ) { return; } // Ignore commonjs optional exports if ( warning.code === "MISSING_EXPORT" && warning.message.includes("__require") ) { return; } // Ignore this warnings if (warning.code === "THIS_IS_UNDEFINED") { return; } // Ignore falsy source map errors if (warning.code === "SOURCEMAP_ERROR") { return; } return handler(warning); }, input: { "server-entry": "fresh:server_entry", }, }, }, }, }, }; }, async configResolved(vConfig) { // Run update check in background updateCheck(UPDATE_INTERVAL).catch(() => {}); fConfig.islandsDir = pathWithRoot(fConfig.islandsDir, vConfig.root); fConfig.routeDir = pathWithRoot(fConfig.routeDir, vConfig.root); config?.islandSpecifiers?.map((spec) => { const specName = specToName(spec); const name = fConfig.namer.getUniqueName(specName); fConfig.islandSpecifiers.set(spec, name); }); const envDir = pathWithRoot( vConfig.envDir || vConfig.root, vConfig.root, ); await loadEnvFile(path.join(envDir, ".env")); await loadEnvFile(path.join(envDir, ".env.local")); const mode = isDev ? "development" : "production"; await loadEnvFile(path.join(envDir, `.env.${mode}`)); await loadEnvFile(path.join(envDir, `.env.${mode}.local`)); }, }, serverEntryPlugin(fConfig), patches(), ...serverSnapshot(fConfig), clientEntryPlugin(fConfig), ...clientSnapshot(fConfig), buildIdPlugin(), ...devServer(), prefresh({ include: [/\.[cm]?[tj]sx?$/], exclude: [/node_modules/, /[\\/]+deno[\\/]+npm[\\/]+/], parserPlugins: [ "importMeta", "explicitResourceManagement", "topLevelAwait", ], }), checkImports({ checks: fConfig.checkImports }), ]; if (typeof process.versions.deno === "string") { plugins.push(deno()); } return plugins; } async function loadEnvFile(envPath: string) { try { await stdLoadEnv({ envPath, export: true }); } catch { // Ignore } } ================================================ FILE: packages/plugin-vite/src/plugins/build_id.ts ================================================ import type { Plugin } from "vite"; export function buildIdPlugin(): Plugin { let buildId = ""; const regex = /^(jsr:)?@fresh\/build-id(@.*)?$/; return { name: "fresh:build-id", sharedDuringBuild: true, async config(_, env) { const isDev = env.command === "serve"; buildId = await getBuildId(isDev); return { define: { BUILD_ID: JSON.stringify(buildId), }, }; }, applyToEnvironment() { return true; }, resolveId(id) { if (regex.test(id)) { return `\0fresh/build-id`; } }, load: { filter: { id: /\0fresh\/build-id/, }, handler() { return `export let BUILD_ID = ${JSON.stringify(buildId)}; export const DENO_DEPLOYMENT_ID = undefined; export function setBuildId(id) { BUILD_ID = id; }`; }, }, }; } export async function getBuildId(dev: boolean): Promise { const gitRevision = Deno.env.get("DENO_DEPLOYMENT_ID") ?? Deno.env.get("DENO_DEPLOY_BUILD_ID") ?? Deno.env.get("GITHUB_SHA") ?? Deno.env.get("CI_COMMIT_SHA"); if (gitRevision !== undefined) { return gitRevision.trim(); } if (!dev) { try { const bin = Deno.build.os === "windows" ? "git.exe" : "git"; const res = await new Deno.Command(bin, { args: ["rev-parse", "HEAD"] }) .output(); return new TextDecoder().decode(res.stdout).trim(); } catch { // ignore } } const arr = new Uint32Array(1); crypto.getRandomValues(arr); return String(arr[0]).trim(); } ================================================ FILE: packages/plugin-vite/src/plugins/client_entry.ts ================================================ import type { Plugin } from "vite"; import { pathWithRoot, type ResolvedFreshViteConfig } from "../utils.ts"; export function clientEntryPlugin(options: ResolvedFreshViteConfig): Plugin { const modName = "fresh:client-entry"; const modNameUser = "fresh:client-entry-user"; let clientEntry = ""; let isDev = false; return { name: "fresh:client-entry", sharedDuringBuild: true, config(_, env) { isDev = env.command === "serve"; }, applyToEnvironment(env) { return env.config.consumer === "client"; }, configResolved(config) { clientEntry = pathWithRoot(options.clientEntry, config.root); }, resolveId: { filter: { id: /(fresh:client-entry|fresh:client-entry-user|fresh:client-quirks)/, }, handler(id) { if (id === modName) { return `\0${modName}`; } else if (id === "fresh:client-quirks") { return "@fresh/plugin-vite/client"; } else if (id === modNameUser) { return clientEntry; } }, }, load: { filter: { id: /\0fresh:client-entry/, }, async handler() { let exists = false; try { const stat = await Deno.stat(clientEntry); exists = stat.isFile; } catch { exists = false; } return ` ${isDev ? 'import "preact/debug"' : ""} export * from "fresh/runtime-client"; ${exists ? `import "fresh:client-entry-user";` : ""} import "@fresh/plugin-vite/client";`; }, }, }; } ================================================ FILE: packages/plugin-vite/src/plugins/client_snapshot.ts ================================================ import type { Plugin, ViteDevServer } from "vite"; import { pathWithRoot, type ResolvedFreshViteConfig } from "../utils.ts"; import { crawlFsItem, specToName } from "fresh/internal-dev"; import * as path from "@std/path"; export function clientSnapshot(options: ResolvedFreshViteConfig): Plugin[] { const modName = "fresh:client-snapshot"; const islands = new Set(); let server: ViteDevServer | undefined; let isDev = false; const entryToIsland = new Map(); return [ { name: "fresh:client-snapshot", sharedDuringBuild: true, applyToEnvironment(env) { return env.config.consumer === "client"; }, async config(cfg, env) { isDev = env.command === "serve"; const cwd = Deno.cwd(); const result = await crawlFsItem({ islandDir: pathWithRoot(options.islandsDir, cfg.root ?? cwd), routeDir: pathWithRoot(options.routeDir, cfg.root ?? cwd), ignore: options.ignore, }); const input: Record = {}; if (isDev) { input["client-snapshot"] = "fresh:client-snapshot"; } for (let i = 0; i < result.islands.length; i++) { const filePath = result.islands[i]; islands.add(filePath); if (!isDev) { const specName = specToName(filePath); const name = options.namer.getUniqueName(specName); entryToIsland.set(name, filePath); input[`fresh-island::${name}`] = `fresh-client-island::${name}`; } } return { environments: { client: { build: { rollupOptions: { input, }, }, }, }, }; }, configResolved(cfg) { for (const [name, spec] of entryToIsland.entries()) { const full = pathWithRoot(spec, cfg.root); entryToIsland.set(name, full); } }, options(opts) { options.islandSpecifiers.forEach((_name, spec) => { islands.add(spec); if (!isDev) { const specName = specToName(spec); const name = options.namer.getUniqueName(specName); entryToIsland.set(name, spec); // deno-lint-ignore no-explicit-any (opts.input as any)[`fresh-island::${name}`] = `fresh-client-island::${name}`; } }); }, configureServer(devServer) { server = devServer; server.watcher.on("add", (filePath) => { if (!isIslandPath(options, filePath)) return; islands.add(filePath); invalidateSnapshots(server!); }); server.watcher.on("unlink", (filePath) => { if (!isIslandPath(options, filePath)) return; islands.delete(filePath); invalidateSnapshots(server!); }); }, resolveId: { filter: { id: /fresh:client-snapshot/, }, handler(id) { if (id === modName) { return `\0${modName}`; } }, }, load: { filter: { id: /\0fresh:client-snapshot/, }, handler() { const imports = Array.from(islands.keys()).map((file, i) => { return `export const mod_${i} = await import(${ JSON.stringify(file) });`; }).join("\n"); if (isDev && server !== undefined) { const mod = server.environments.ssr.moduleGraph.getModuleById( "\0fresh:server-snapshot", ); if (mod) { server.environments.ssr.moduleGraph.invalidateModule(mod); } } return `${imports} if (import.meta.hot) { import.meta.hot.accept(() => { console.log("accepting client-snapshot") }); } `; }, }, }, { name: "fresh:client-island", sharedDuringBuild: true, resolveId: { filter: { id: /^fresh-client-island::/, }, handler(id) { const name = id.slice("fresh-client-island::".length); const full = entryToIsland.get(name); return full; }, }, }, ]; } function isIslandPath( options: ResolvedFreshViteConfig, filePath: string, ): boolean { const relIsland = path.relative(options.islandsDir, filePath); if (!relIsland.startsWith("..")) return true; const relRoutes = path.relative(options.routeDir, filePath); if (!relIsland.startsWith("..") && relRoutes.includes("(_islands)")) { return true; } return false; } function invalidateSnapshots(server: ViteDevServer) { const client = server.environments.client.moduleGraph.getModuleById( "\0fresh:client-snapshot", ); if (client !== undefined) { server.environments.client.moduleGraph.invalidateModule(client); } const ssr = server.environments.ssr.moduleGraph.getModuleById( "\0fresh:server-snapshot", ); if (ssr !== undefined) { server.environments.ssr.moduleGraph.invalidateModule(ssr); } } ================================================ FILE: packages/plugin-vite/src/plugins/deno.ts ================================================ import type { Plugin } from "vite"; import { type Loader, MediaType, RequestedModuleType, ResolutionMode, Workspace, } from "@deno/loader"; import * as path from "@std/path"; import * as babel from "@babel/core"; import { httpAbsolute } from "./patches/http_absolute.ts"; import { JS_REG, JSX_REG } from "../utils.ts"; import { builtinModules } from "node:module"; // @ts-ignore Workaround for https://github.com/denoland/deno/issues/30850 const { default: babelReact } = await import("@babel/preset-react"); const BUILTINS = new Set(builtinModules); interface DenoState { type: RequestedModuleType; } export function deno(): Plugin { let ssrLoader: Loader; let browserLoader: Loader; let isDev = false; return { name: "deno", sharedDuringBuild: true, // We must be first to be able to resolve before the // Vite's own`vite:resolve` plugin. It always treats bare // specifiers as external during SSR. enforce: "pre", config(_, env) { isDev = env.command === "serve"; }, async configResolved() { // TODO: Pass conditions ssrLoader = await new Workspace({ platform: "node", cachedOnly: true, }).createLoader(); browserLoader = await new Workspace({ platform: "browser", preserveJsx: true, cachedOnly: true, }) .createLoader(); }, applyToEnvironment() { return true; }, async resolveId(id, importer, options) { if (BUILTINS.has(id)) { // `node:` prefix is not included in builtins list. if (!id.startsWith("node:")) { id = `node:${id}`; } return { id, external: true, }; } const loader = this.environment.config.consumer === "server" ? ssrLoader : browserLoader; const original = id; let isHttp = false; if (id.startsWith("deno-http::")) { isHttp = true; id = id.slice("deno-http::".length); } importer = isDenoSpecifier(importer) ? parseDenoSpecifier(importer).specifier : importer; if (id.startsWith("/") && importer && /^https?:\/\//g.test(importer)) { const url = new URL(importer); id = `${url.origin}${id}`; } // We still want to allow other plugins to participate in // resolution, with us being in front due to `enforce: "pre"`. // But we still want to ignore everything `vite:resolve` does // because we're kinda replacing that plugin here. const tmp = await this.resolve(id, importer, options); if (tmp && tmp.resolvedBy !== "vite:resolve") { if (tmp.external && !/^https?:\/\//.test(tmp.id)) { return tmp; } // A plugin namespaced it, we should not attempt to resolve it. if (tmp.id.startsWith("\0")) { return tmp; } id = tmp.id; } // Plugins may return lower cased drive letters on windows if (!isHttp && path.isAbsolute(id)) { id = path.toFileUrl(path.normalize(id)) .href; } try { // Ensure we're passing a valid importer that Deno understands const denoImporter = importer && !importer.startsWith("\0") ? importer : undefined; let resolved = await loader.resolve( id, denoImporter, ResolutionMode.Import, ); if (resolved.startsWith("node:")) { return { id: resolved, external: true, }; } if (original === resolved) { return null; } const type = getDenoType(id, options.attributes.type ?? "default"); if ( type !== RequestedModuleType.Default || /^(https?|jsr|npm):/.test(resolved) ) { return toDenoSpecifier(resolved, type); } if (resolved.startsWith("file://")) { resolved = path.fromFileUrl(resolved); } return { id: resolved, meta: { deno: { type, }, }, }; } catch { // ignore } }, async load(id) { const loader = this.environment.config.consumer === "server" ? ssrLoader : browserLoader; if (isDenoSpecifier(id)) { const { type, specifier } = parseDenoSpecifier(id); const result = await loader.load(specifier, type); if (result.kind === "external") { return null; } const code = new TextDecoder().decode(result.code); const maybeJsx = babelTransform({ ssr: this.environment.config.consumer === "server", media: result.mediaType, code, id: specifier, isDev, }); if (maybeJsx !== null) { return maybeJsx; } return { code, }; } if (id.startsWith("\0")) { id = id.slice(1); } const meta = this.getModuleInfo(id)?.meta.deno as | DenoState | undefined | null; if (meta === null || meta === undefined) return; // Skip for non-js files like `.css` if ( meta.type === RequestedModuleType.Default && !JS_REG.test(id) ) { return; } const url = path.toFileUrl(id); const result = await loader.load(url.href, meta.type); if (result.kind === "external") { return null; } const code = new TextDecoder().decode(result.code); const maybeJsx = babelTransform({ ssr: this.environment.config.consumer === "server", media: result.mediaType, id, code, isDev, }); if (maybeJsx) { return maybeJsx; } return { code, }; }, transform: { filter: { id: JSX_REG, }, async handler(_, id) { // This transform is a hack to be able to re-use Deno's precompile // jsx transform. if (this.environment.name === "client") { return; } let actualId = id; if (isDenoSpecifier(id)) { const { specifier } = parseDenoSpecifier(id); actualId = specifier; } actualId = actualId.replace("?commonjs-es-import", ""); if (actualId.startsWith("\0")) { actualId = actualId.slice(1); } if (path.isAbsolute(actualId)) { actualId = path.toFileUrl(actualId).href; } const resolved = await ssrLoader.resolve( actualId, undefined, ResolutionMode.Import, ); const result = await ssrLoader.load( resolved, RequestedModuleType.Default, ); if (result.kind === "external") { return; } const code = new TextDecoder().decode(result.code); return { code, }; }, }, }; } function isJsMediaType(media: MediaType): boolean { switch (media) { case MediaType.JavaScript: case MediaType.Jsx: case MediaType.Mjs: case MediaType.Cjs: case MediaType.TypeScript: case MediaType.Mts: case MediaType.Cts: case MediaType.Tsx: return true; case MediaType.Dts: case MediaType.Dmts: case MediaType.Dcts: case MediaType.Css: case MediaType.Json: case MediaType.Html: case MediaType.Sql: case MediaType.Wasm: case MediaType.SourceMap: case MediaType.Unknown: return false; } } export type DenoSpecifier = string & { __deno: string }; function isDenoSpecifier(str: unknown): str is DenoSpecifier { return typeof str === "string" && str.startsWith("\0deno::"); } function toDenoSpecifier(spec: string, type: RequestedModuleType) { return `\0deno::${type}::${spec}`; } function parseDenoSpecifier( spec: DenoSpecifier, ): { type: RequestedModuleType; specifier: string } { const match = spec.match(/^\0deno::([^:]+)::(.*)$/)!; let specifier = match[2]; const specMatch = specifier.match(/^(\w+):\/([^/].*)$/); if (specMatch !== null) { const protocol = specMatch[1]; let rest = specMatch[2]; if (protocol === "file") { rest = "/" + rest; } specifier = `${protocol}://${rest}`; } if (path.isAbsolute(specifier)) { specifier = path.toFileUrl(specifier).href; } return { type: +match[1], specifier }; } function getDenoType(id: string, type: string): RequestedModuleType { switch (type) { case "json": return RequestedModuleType.Json; case "bytes": return RequestedModuleType.Bytes; case "text": return RequestedModuleType.Text; default: if (id.endsWith(".json")) { return RequestedModuleType.Json; } return RequestedModuleType.Default; } } function babelTransform( options: { media: MediaType; ssr: boolean; code: string; id: string; isDev: boolean; }, ) { if (!isJsMediaType(options.media)) { return null; } const { ssr, code, id, isDev } = options; const presets: babel.PluginItem[] = []; if ( !ssr && id.endsWith(".tsx") || id.endsWith(".jsx") ) { presets.push([babelReact, { runtime: "automatic", importSource: "preact", development: isDev, throwIfNamespace: false, }]); } const url = URL.canParse(id) ? new URL(id) : null; const result = babel.transformSync(code, { filename: id, babelrc: false, sourceMaps: "both", presets: presets, plugins: [httpAbsolute(url)], compact: false, }); if (result !== null && result.code) { return { code: result.code, map: result.map, }; } return null; } ================================================ FILE: packages/plugin-vite/src/plugins/dev_server.ts ================================================ import type { DevEnvironment, Plugin } from "vite"; import * as path from "@std/path"; import { ASSET_CACHE_BUST_KEY } from "fresh/internal"; import { createRequest, sendResponse } from "@remix-run/node-fetch-server"; import { hashCode } from "../shared.ts"; export function devServer(): Plugin[] { let publicDir = ""; return [ { name: "fresh:dev_server", sharedDuringBuild: true, configResolved(config) { publicDir = config.publicDir; }, configureServer(server) { const IGNORE_URLS = /^\/(@(vite|fs|id)|\.vite)\//; server.middlewares.use(async (nodeReq, nodeRes, next) => { const serverCfg = server.config.server; const protocol = serverCfg.https ? "https" : "http"; const host = serverCfg.host ? serverCfg.host : "localhost"; const port = serverCfg.port; const url = new URL( `${protocol}://${host}:${port}${nodeReq.url ?? "/"}`, ); // Don't cache in dev url.searchParams.delete(ASSET_CACHE_BUST_KEY); // Check if it's a vite url if ( IGNORE_URLS.test(url.pathname) || server.environments.client.moduleGraph.urlToModuleMap.has( url.pathname, ) || server.environments.ssr.moduleGraph.urlToModuleMap.has( url.pathname, ) || url.pathname === "/.well-known/appspecific/com.chrome.devtools.json" ) { return next(); } // Check if it's a static file first const staticFilePath = path.join(publicDir, url.pathname.slice(1)); try { const stat = await Deno.stat(staticFilePath); if (stat.isFile) { return next(); } } catch { // Ignore } // Check if it's a static/index.html file const staticFilePathIndex = path.join( publicDir, url.pathname.slice(1), "index.html", ); try { const content = await Deno.readTextFile(staticFilePathIndex); nodeRes.setHeader("Content-Type", "text/html; charset=utf-8"); nodeRes.end(content); return; } catch { // Ignore } try { const mod = await server.ssrLoadModule("fresh:server_entry"); const req = createRequest(nodeReq, nodeRes); mod.setErrorInterceptor((err: unknown) => { if (err instanceof Error) { server.ssrFixStacktrace(err); } }); const res = (await mod.default.fetch(req)) as Response; // Collect css eagerly to avoid FOUC. This is a workaround for // Vite not supporting css natively. It's a bit hacky, but // gets the job done. if ( url.pathname !== "/__inspect" && res.headers.get("Content-Type")?.includes("text/html") ) { const collected = await collectCss( "fresh:client-entry", server.environments.client, ); let html = await res.text(); const styles = collected.join("\n"); html = html.replace("", styles + ""); const newRes = new Response(html, { status: res.status, headers: res.headers, }); await sendResponse(nodeRes, newRes); return; } await sendResponse(nodeRes, res); } catch (err) { if (err instanceof Error) { server.ssrFixStacktrace(err); } return next(err); } }); }, }, { name: "fresh:server_hmr", applyToEnvironment(env) { return env.config.consumer === "server"; }, hotUpdate(options) { const clientMod = options.server.environments.client.moduleGraph .getModulesByFile(options.file); if (clientMod !== undefined) { // Vite can do HMR here return; } const ssrMod = options.server.environments.ssr.moduleGraph .getModulesByFile(options.file); if (ssrMod !== undefined) { // SSR-only module. Might still be a route. // TODO: Implement proper ssr hmr options.server.hot.send("fresh:reload"); } }, }, ]; } async function collectCss( id: string, env: DevEnvironment, ) { const seen = new Set(); const queue: string[] = [id]; const out: string[] = []; let current: string | undefined; while ((current = queue.pop()) !== undefined) { if (seen.has(current)) continue; seen.add(current); let mod = env.moduleGraph.idToModuleMap.get(current); if (mod === undefined || mod.transformResult === null) { // During development assets are loaded lazily, so we need // to trigger processing manually. await env.fetchModule(current); mod = env.moduleGraph.idToModuleMap.get(current) ?? env.moduleGraph.idToModuleMap.get(`\0${current}`); if (mod === undefined) continue; } if ( (current.endsWith(".scss") || current.endsWith(".css")) && mod.transformResult ) { // Since vite stores everything as a JS file we need to // extract the CSS out of the JS const match = mod.transformResult.code.match( /__vite__css\s+=\s+("(?:\\\\|\\"|[^"])*")/, ); if (match !== null) { const content = JSON.parse(match[1]); out.push( ``, ); } } mod.importedModules.forEach((m) => { if (m.id === null) return; queue.push(m.id); }); } return out; } ================================================ FILE: packages/plugin-vite/src/plugins/patches/code_eval.ts ================================================ import type { PluginObj, PluginPass, types } from "@babel/core"; const APPLY_PG_QUIRKS = "applyPgQuirks"; export function codeEvalPlugin( env: "server" | "client", mode: string, ) { return ( { types: t }: { types: typeof types }, ): PluginObj => { return { name: "fresh-remove-polyfills", visitor: { IfStatement: { enter(path, state) { const res = evaluateExpr(t, env, mode, path.node.test, state); // Could not evaluate if (res === null) return; if (res) { if (t.isBlockStatement(path.node.consequent)) { path.replaceWithMultiple(path.node.consequent.body); } else { path.replaceWith(path.node.consequent); } } else if (path.node.alternate) { if (t.isBlockStatement(path.node.alternate)) { path.replaceWithMultiple(path.node.alternate.body); } else { path.replaceWith(path.node.alternate); } } else { path.remove(); } }, }, }, }; }; } function evaluateExpr( t: typeof types, env: "server" | "client", mode: string, node: types.Node, state: PluginPass, ): boolean | null { if (t.isLogicalExpression(node)) { const left = evaluateExpr(t, env, mode, node.left, state); if (left === null) return null; if (left && node.operator === "||") return true; const right = evaluateExpr(t, env, mode, node.right, state); if (right === null) return null; switch (node.operator) { case "||": return left || right; case "&&": return left && right; case "??": return left ?? right; default: return false; } } else if (t.isBinaryExpression(node)) { // Check: typeof process == "undefined" // Check: typeof process === "undefined" // Check: typeof process != "undefined" // Check: typeof process !== "undefined" if ( t.isUnaryExpression(node.left) && node.left.operator === "typeof" && t.isIdentifier(node.left.argument) && node.left.argument.name === "process" && t.isStringLiteral(node.right) && node.right.value === "undefined" ) { if (node.operator === "==" || node.operator === "===") { return env !== "server"; } else if (node.operator === "!=" || node.operator === "!==") { return env === "server"; } } else if ( // Workaround for npm:pg // Check: typeof process.env.NODE_PG_FORCE_NATIVE !== "undefined" t.isUnaryExpression(node.left) && t.isMemberExpression(node.left.argument) && t.isMemberExpression(node.left.argument.object) && t.isIdentifier(node.left.argument.object.object) && node.left.argument.object.object.name === "process" && t.isIdentifier(node.left.argument.object.property) && node.left.argument.object.property.name === "env" && t.isIdentifier(node.left.argument.property) && node.left.argument.property.name === "NODE_PG_FORCE_NATIVE" && t.isStringLiteral(node.right) ) { state.set(APPLY_PG_QUIRKS, true); const left = typeof Deno.env.get("NODE_PG_FORCE_NATIVE"); const right = node.right.value; const result = applyBinExpr(left, right, node); if (result !== null) return result; } else if ( // Check: process.env.NODE_ENV == "production" // Check: process.env.NODE_ENV === "production" // Check: process.env.NODE_ENV != "production" // Check: process.env.NODE_ENV !== "production" // Check: process.env.NODE_ENV == "development" // Check: process.env.NODE_ENV === "development" // Check: process.env.NODE_ENV != "development" // Check: process.env.NODE_ENV !== "development" t.isMemberExpression(node.left) && t.isMemberExpression(node.left.object) && t.isIdentifier(node.left.object.object) && node.left.object.object.name === "process" && t.isIdentifier(node.left.object.property) && node.left.object.property.name === "env" && t.isIdentifier(node.left.property) && node.left.property.name === "NODE_ENV" && t.isStringLiteral(node.right) ) { const value = node.right.value; const result = applyBinExpr(mode, value, node); if (result !== null) return result; } else if ( // Check: process.foo === "bar" env === "server" && t.isMemberExpression(node.left) && t.isIdentifier(node.left.object) && node.left.object.name === "process" && t.isIdentifier(node.left.property) && !PROCESS_PROPERTIES.has(node.left.property.name) ) { return false; } return null; } else if ( t.isMemberExpression(node) && t.isIdentifier(node.object) && node.object.name === "process" && t.isIdentifier(node.property) ) { return PROCESS_PROPERTIES.has(node.property.name); } else if (t.isIdentifier(node) && node.name === "useLegacyCrypto") { return false; } return null; } function applyBinExpr( left: unknown, right: unknown, node: types.BinaryExpression, ): boolean | null { switch (node.operator) { case "==": return left == right; case "===": return left === right; case "!=": return left != right; case "!==": return left !== right; case "+": case "-": case "/": case "%": case "*": case "**": case "&": case "|": case ">>": case ">>>": case "<<": case "^": case "in": case "instanceof": case ">": case "<": case ">=": case "<=": case "|>": break; } return null; } const PROCESS_PROPERTIES = new Set([ "abort", "allowedNodeEnvironmentFlags", "arch", "argv", "argv0", "availableMemory", "channel", "chdir", "config", "connected", "constrainedMemory", "cpuUsage", "cwd", "debugPort", "disconnect", "dlopen", "emitWarning", "env", "execArgv", "execPath", "execve", "exit", "exitCode", "features", "finalization", "getActiveResourcesInfo", "getBuiltinModule", "getegid", "geteuid", "getgid", "getgroups", "getuid", "hasUncaughtExceptionCaptureCallback", "hrtime", "initgroups", "kill", "loadEnvFile", "mainModule", "memoryUsage", "nextTick", "noDeprecation", "permission", "pid", "platform", "ppid", "ref", "release", "report", "resourceUsage", "send", "setegid", "seteuid", "setgid", "setgroups", "setuid", "setSourceMapsEnabled", "setUncaughtExceptionCaptureCallback", "sourceMapsEnabled", "stderr", "stdin", "stdout", "throwDeprecation", "threadCpuUsage", "title", "traceDeprecation", "umask", "unref", "uptime", "version", "versions", ]); ================================================ FILE: packages/plugin-vite/src/plugins/patches/code_eval_test.ts ================================================ import { expect } from "@std/expect/expect"; import * as babel from "@babel/core"; import { codeEvalPlugin } from "./code_eval.ts"; function runTest( options: { input: string; expected: string; env: "client" | "server"; mode: "development" | "production"; }, ) { const res = babel.transformSync(options.input, { filename: "foo.js", babelrc: false, plugins: [codeEvalPlugin(options.env, options.mode)], }); const output = res?.code ?? ""; expect(output).toEqual(options.expected); } Deno.test("code eval - resolve runtime conditional detection", () => { runTest({ env: "client", mode: "development", input: `if (typeof process === 'undefined' || process.type === 'renderer' || process.browser === true || process.__nwjs) { module.exports = require('./browser.js'); } else { module.exports = require('./node.js'); }`, expected: `module.exports = require('./browser.js');`, }); runTest({ env: "client", mode: "development", input: `if (typeof process !== 'undefined') { module.exports = require('./node.js'); } else { module.exports = require('./browser.js'); }`, expected: `module.exports = require('./browser.js');`, }); runTest({ env: "server", mode: "development", input: `if (typeof process === 'undefined') { module.exports = require('./browser.js'); } else { module.exports = require('./node.js'); }`, expected: `module.exports = require('./node.js');`, }); runTest({ env: "server", mode: "development", input: `if (typeof process !== 'undefined') { module.exports = require('./node.js'); } else { module.exports = require('./browser.js'); }`, expected: `module.exports = require('./node.js');`, }); }); Deno.test("code eval - resolve process.env.NODE_ENV", () => { runTest({ env: "client", mode: "development", input: `if (process.env.NODE_ENV === 'production') { module.exports = require('./cjs/react.production.js'); } else { module.exports = require('./cjs/react.development.js'); }`, expected: `module.exports = require('./cjs/react.development.js');`, }); runTest({ env: "client", mode: "production", input: `if (process.env.NODE_ENV === 'production') { module.exports = require('./cjs/react.production.js'); } else { module.exports = require('./cjs/react.development.js'); }`, expected: `module.exports = require('./cjs/react.production.js');`, }); }); Deno.test("code eval - npm:debug", () => { runTest({ env: "server", mode: "development", input: `if (typeof process === 'undefined' || process.type === 'renderer' || process.browser === true || process.__nwjs) { module.exports = require('./browser.js'); } else { module.exports = require('./node.js'); }`, expected: `module.exports = require('./node.js');`, }); }); Deno.test("code eval - npm:pg", () => { runTest({ env: "server", mode: "development", input: `if (typeof process.env.NODE_PG_FORCE_NATIVE !== "undefined") { module.exports = require('./native.js'); } else { module.exports = require('./normal.js'); }`, expected: `module.exports = require('./normal.js');`, }); }); Deno.test("code eval - npm:pg #2", () => { runTest({ env: "server", mode: "development", input: `const useLegacyCrypto = parseInt(process.versions && process.versions.node && process.versions.node.split('.')[0]) < 15 if (useLegacyCrypto) { // We are on an old version of Node.js that requires legacy crypto utilities. module.exports = require('./utils-legacy') } else { module.exports = require('./utils-webcrypto') } `, expected: `const useLegacyCrypto = parseInt(process.versions && process.versions.node && process.versions.node.split('.')[0]) < 15; module.exports = require('./utils-webcrypto');`, }); }); ================================================ FILE: packages/plugin-vite/src/plugins/patches/commonjs.ts ================================================ import type { NodePath, PluginObj, types } from "@babel/core"; import { builtinModules } from "node:module"; const BUILTINS = new Set(builtinModules); export function cjsPlugin( { types: t }: { types: typeof types }, ): PluginObj { const HAS_ES_MODULE = "esModule"; const REQUIRE_CALLS = "requireCalls"; const ROOT_SCOPE = "rootScope"; const EXPORTED = "exported"; const EXPORTED_NAMESPACES = "exported_namespaces"; const ALIASED = "aliased"; const REEXPORT = "re-export"; const NEEDS_REQUIRE_IMPORT = "needsRequireImport"; const IS_ESM = "isESM"; return { name: "fresh-cjs-esm", pre(file) { const filename = file.opts.filename; if (filename) { if (filename.endsWith(".mjs") || filename.endsWith(".cts")) { this.set(IS_ESM, true); } else if (filename.endsWith(".cjs") || filename.endsWith(".cts")) { this.set(IS_ESM, false); } } }, visitor: { Program: { enter(path, state) { state.set(ROOT_SCOPE, path.scope); state.set(EXPORTED, new Set()); state.set(EXPORTED_NAMESPACES, new Set()); state.set(REEXPORT, null); path.traverse({ Import(_path, state) { state.set(IS_ESM, true); }, ImportDeclaration(_path, state) { state.set(IS_ESM, true); }, ExportAllDeclaration(_path, state) { state.set(IS_ESM, true); }, ExportDefaultDeclaration(_path, state) { state.set(IS_ESM, true); }, ExportNamedDeclaration(_path, state) { state.set(IS_ESM, true); }, }, state); }, exit(path, state) { const isESM = state.get(IS_ESM); if (isESM) return; const body = path.get("body"); const requires = state.get(REQUIRE_CALLS); if (requires !== undefined) { for (let i = 0; i < requires.length; i++) { const { specifier, id } = requires[i]; path.unshiftContainer( "body", t.importDeclaration( [t.importNamespaceSpecifier(id)], specifier, ), ); } } const reexport = state.get(REEXPORT); const exported = state.get(EXPORTED); const exportedNs = state.get(EXPORTED_NAMESPACES); const needsRequireImport = state.get(NEEDS_REQUIRE_IMPORT); const hasEsModule = state.get(HAS_ES_MODULE); if (needsRequireImport) { // Inject: // ```ts // import { createRequire } from "node:module"; // const require = createRequire(import.meta.url); // ``` const id = t.identifier("createRequire"); path.unshiftContainer( "body", t.variableDeclaration("const", [ t.variableDeclarator( t.identifier("require"), t.callExpression(t.identifier("createRequire"), [ t.memberExpression( t.metaProperty( t.identifier("import"), t.identifier("meta"), ), t.identifier("url"), ), ]), ), ]), ); path.unshiftContainer( "body", t.importDeclaration( [t.importSpecifier(id, id)], t.stringLiteral("node:module"), ), ); } if (reexport !== null) { path.unshiftContainer( "body", t.exportAllDeclaration(t.cloneNode(reexport, true)), ); } const mappedNs: string[] = []; for (const spec of exportedNs.values()) { const id = path.scope.generateUidIdentifier("__ns"); mappedNs.push(id.name); path.unshiftContainer( "body", t.importDeclaration( [t.importNamespaceSpecifier(id)], t.stringLiteral(spec), ), ); } if (exported.size > 0 || exportedNs.size > 0 || hasEsModule) { path.unshiftContainer( "body", t.expressionStatement( t.callExpression( t.memberExpression( t.identifier("Object"), t.identifier("defineProperty"), ), [ t.identifier("exports"), t.stringLiteral("__esModule"), t.objectExpression([ t.objectProperty( t.identifier("value"), t.booleanLiteral(true), ), ]), ], ), ), ); path.unshiftContainer( "body", t.expressionStatement( t.callExpression( t.memberExpression( t.identifier("Object"), t.identifier("defineProperty"), ), [ t.identifier("module"), t.stringLiteral("exports"), t.objectExpression([ t.objectMethod( "method", t.identifier("get"), [], t.blockStatement([ t.returnStatement(t.identifier("exports")), ]), ), t.objectMethod( "method", t.identifier("set"), [t.identifier("value")], t.blockStatement([ t.expressionStatement( t.assignmentExpression( "=", t.identifier("exports"), t.identifier("value"), ), ), ]), ), ]), ], ), ), ); path.unshiftContainer( "body", t.variableDeclaration("var", [ t.variableDeclarator( t.identifier("exports"), t.objectExpression([]), ), t.variableDeclarator( t.identifier("module"), t.objectExpression([]), ), ]), ); } const exportNamed = new Map(); const idExports: types.ExportSpecifier[] = []; for (const name of exported) { if (name === "default") { exportNamed.set(name, name); continue; } const id = path.scope.generateUidIdentifier(name); exportNamed.set(id.name, name); path.pushContainer( "body", t.variableDeclaration( "var", [t.variableDeclarator( id, t.memberExpression( t.identifier("exports"), t.identifier(name), ), )], ), ); idExports.push( t.exportSpecifier(id, t.identifier(name)), ); } if (idExports.length > 0) { path.pushContainer( "body", t.exportNamedDeclaration(null, idExports), ); } if (exportNamed.size > 0 || exportedNs.size > 0 || hasEsModule) { const id = path.scope.generateUidIdentifier("__default"); path.pushContainer( "body", t.variableDeclaration("const", [ t.variableDeclarator( id, t.logicalExpression( "??", t.memberExpression( t.identifier("exports"), t.identifier("default"), ), t.identifier("exports"), ), ), ]), ); for (let i = 0; i < mappedNs.length; i++) { const mapped = mappedNs[i]; const key = path.scope.generateUid("k"); path.pushContainer( "body", t.forInStatement( t.variableDeclaration("var", [ t.variableDeclarator(t.identifier(key)), ]), t.identifier(mapped), t.ifStatement( t.logicalExpression( "&&", t.logicalExpression( "&&", t.binaryExpression( "!==", t.identifier(key), t.stringLiteral("default"), ), t.binaryExpression( "!==", t.identifier(key), t.stringLiteral("__esModule"), ), ), t.callExpression( t.memberExpression( t.memberExpression( t.memberExpression( t.identifier("Object"), t.identifier("prototype"), ), t.identifier("hasOwnProperty"), ), t.identifier("call"), ), [t.identifier(mapped), t.identifier(key)], ), ), t.expressionStatement( t.assignmentExpression( "=", t.memberExpression( t.cloneNode(id, true), t.identifier(key), true, ), t.memberExpression( t.identifier(mapped), t.identifier(key), true, ), ), ), ), ), ); } path.pushContainer("body", t.exportDefaultDeclaration(id)); path.pushContainer( "body", t.exportNamedDeclaration( t.variableDeclaration("var", [ t.variableDeclarator( t.identifier("__require"), t.identifier("exports"), ), ]), ), ); } if (body.length === 0 && hasEsModule) { path.pushContainer("body", t.exportNamedDeclaration(null)); } else if (hasEsModule) { path.pushContainer( "body", t.exportNamedDeclaration( t.variableDeclaration( "var", [t.variableDeclarator( t.identifier("__esModule"), t.memberExpression( t.identifier("exports"), t.identifier("__esModule"), ), )], ), ), ); } }, }, CallExpression(path, state) { if (state.get(IS_ESM)) return; const exported = state.get(EXPORTED); if (isObjEsModuleFlag(t, path.node)) { state.set(HAS_ES_MODULE, true); return; } if ( t.isIdentifier(path.node.callee) && path.node.callee.name === "require" ) { const root = state.get(ROOT_SCOPE); const id = root.generateUidIdentifier("mod"); const mods = state.get(REQUIRE_CALLS) ?? []; state.set(REQUIRE_CALLS, mods); const source = path.node.arguments[0]; if (t.isStringLiteral(source)) { // Check if we can hoist it or if we need to keep it. let canImport = true; let parent: NodePath | null = path.parentPath; while (parent !== null) { if ( t.isTryStatement(parent.node) || t.isIfStatement(parent.node) || t.isConditionalExpression(parent.node) ) { canImport = false; break; } parent = parent.parentPath; } if (!canImport) { state.set(NEEDS_REQUIRE_IMPORT, true); return; } mods.push({ id, specifier: t.cloneNode(path.node.arguments[0], true), }); if ( path.parentPath?.isVariableDeclarator() && path.parentPath?.get("id").isIdentifier() || path.parentPath?.isCallExpression() ) { // Vite json processing always adds a default property. if (source.value.endsWith(".json")) { path.replaceWith( t.logicalExpression( "??", t.memberExpression( t.cloneNode(id, true), t.identifier("default"), ), t.cloneNode(id, true), ), ); } else if ( path.parentPath?.isCallExpression() && t.isIdentifier(path.parentPath.node.callee) && path.parentPath.node.callee.name === "__importDefault" ) { if (isNodeBuiltin(source.value)) { path.replaceWith(t.objectExpression([ t.objectProperty( t.identifier("__esModule"), t.booleanLiteral(true), ), t.objectProperty( t.identifier("default"), t.logicalExpression( "??", t.memberExpression( t.cloneNode(id, true), t.identifier("default"), ), t.cloneNode(id, true), ), ), ])); } else { path.replaceWith(t.cloneNode(id, true)); } } else { path.replaceWith( t.logicalExpression( "??", t.memberExpression( t.cloneNode(id, true), t.identifier("__require"), ), t.logicalExpression( "??", t.memberExpression( t.cloneNode(id, true), t.identifier("default"), ), t.cloneNode(id, true), ), ), ); } return; } path.replaceWith(t.cloneNode(id, true)); } else { state.set(NEEDS_REQUIRE_IMPORT, true); } } else if ( t.isMemberExpression(path.node.callee) && t.isIdentifier(path.node.callee.object) && path.node.callee.object.name === "Object" && t.isIdentifier(path.node.callee.property) && path.node.callee.property.name === "defineProperty" && path.node.arguments.length > 0 && t.isIdentifier(path.node.arguments[0]) && path.node.arguments[0].name === "exports" && t.isStringLiteral(path.node.arguments[1]) ) { const name = path.node.arguments[1].value; exported.add(name); } }, EmptyStatement(path) { path.remove(); }, MemberExpression: { exit(path, state) { if (state.get(IS_ESM)) return; if ( t.isIdentifier(path.node.object) && path.node.object.name === "exports" && t.isIdentifier(path.node.property) ) { const name = t.cloneNode(path.node.property); if (name.name === "__esModule") return; state.get(EXPORTED).add(name.name); } }, }, ExpressionStatement: { enter(path, state) { if (state.get(IS_ESM)) return; // Check: Object.defineProperty(module.exports) "__esModule" ...) // Check: Object.defineProperty(exports) "__esModule" ...) // Check: a({}, "__esModule", ...) if ( t.isCallExpression(path.node.expression) && path.node.expression.arguments.length === 3 && t.isStringLiteral(path.node.expression.arguments[1]) && path.node.expression.arguments[1].value === "__esModule" ) { state.set(HAS_ES_MODULE, true); return; } if ( t.isExpressionStatement(path.node) && t.isCallExpression(path.node.expression) && t.isIdentifier(path.node.expression.callee) && path.node.expression.callee.name === "__exportStar" && path.node.expression.arguments.length > 0 && t.isCallExpression(path.node.expression.arguments[0]) && t.isIdentifier(path.node.expression.arguments[0].callee) && path.node.expression.arguments[0].callee.name === "require" && t.isStringLiteral(path.node.expression.arguments[0].arguments[0]) ) { const spec = t.cloneNode( path.node.expression.arguments[0].arguments[0], true, ); state.get(EXPORTED_NAMESPACES).add(spec.value); path.replaceWith(t.exportAllDeclaration(spec)); } else if ( t.isExpressionStatement(path.node) && t.isCallExpression(path.node.expression) && t.isFunctionExpression(path.node.expression.callee) ) { if ( path.node.expression.callee.params.length > 0 && t.isIdentifier(path.node.expression.callee.params[0]) ) { const alias = path.node.expression.callee.params[0].name; state.set(ALIASED, alias); } } else if ( // Check: Object.defineProperty(exports, "foo", { enumerable: true, get: function () { return foo; } }); t.isCallExpression(path.node.expression) && t.isMemberExpression(path.node.expression.callee) && t.isIdentifier(path.node.expression.callee.object) && path.node.expression.callee.object.name === "Object" && t.isIdentifier(path.node.expression.callee.property) && path.node.expression.callee.property.name === "defineProperty" && path.node.expression.arguments.length >= 2 && t.isIdentifier(path.node.expression.arguments[0]) && path.node.expression.arguments[0].name === "exports" && t.isStringLiteral(path.node.expression.arguments[1]) && t.isObjectExpression(path.node.expression.arguments[2]) ) { const exported = path.node.expression.arguments[1].value; const obj = path.node.expression.arguments[2]; for (let i = 0; i < obj.properties.length; i++) { const prop = obj.properties[i]; if ( t.isObjectProperty(prop) && t.isIdentifier(prop.key) && prop.key.name === "get" && t.isFunctionExpression(prop.value) && t.isBlockStatement(prop.value.body) && prop.value.body.body.length === 1 && t.isReturnStatement(prop.value.body.body[0]) ) { const expr = prop.value.body.body[0].argument; if (expr !== null && expr !== undefined) { path.replaceWith( t.assignmentExpression( "=", t.memberExpression( t.identifier("exports"), t.identifier(exported), ), t.cloneNode(expr, true), ), ); } } else if ( t.isObjectMethod(prop) && t.isIdentifier(prop.key) && prop.key.name === "get" && t.isBlockStatement(prop.body) && prop.body.body.length === 1 && t.isReturnStatement(prop.body.body[0]) ) { const expr = prop.body.body[0].argument; if (expr !== null && expr !== undefined) { path.replaceWith( t.assignmentExpression( "=", t.memberExpression( t.identifier("exports"), t.identifier(exported), ), t.cloneNode(expr, true), ), ); } } } } else if ( // Check: module.exports = require(...) t.isAssignmentExpression(path.node.expression) && t.isMemberExpression(path.node.expression.left) && t.isIdentifier(path.node.expression.left.object) && t.isIdentifier(path.node.expression.left.property) && path.node.expression.left.object.name === "module" && path.node.expression.left.property.name === "exports" && t.isCallExpression(path.node.expression.right) && t.isIdentifier(path.node.expression.right.callee) && path.node.expression.right.callee.name === "require" && path.node.expression.right.arguments.length === 1 && t.isStringLiteral(path.node.expression.right.arguments[0]) ) { const source = path.node.expression.right.arguments[0]; state.set(REEXPORT, source); } else { let depth = 0; let current = path.node.expression; while ( t.isAssignmentExpression(current) && t.isMemberExpression(current.left) && t.isIdentifier(current.left.object) && current.left.object.name === "exports" ) { if ( t.isUnaryExpression(current.right) && current.right.operator === "void" && t.isNumericLiteral(current.right.argument) && current.right.argument.value === 0 ) { if (depth > 0) { path.remove(); } break; } depth++; current = current.right; } } }, exit(path, state) { if (state.get(IS_ESM)) return; const exported = state.get(EXPORTED); const expr = path.get("expression"); if (expr.isAssignmentExpression()) { const left = expr.get("left"); if (isEsModuleFlag(t, expr.node)) { state.set(HAS_ES_MODULE, true); } else if (left.isMemberExpression()) { if (isModuleExports(t, left.node)) { exported.add("default"); if (t.isObjectExpression(expr.node.right)) { const properties = expr.node.right.properties; for (let i = 0; i < properties.length; i++) { const prop = properties[i]; if (t.isObjectProperty(prop)) { if (t.isIdentifier(prop.key)) { if (prop.key.name === "__esModule") { continue; } exported.add(prop.key.name); } } } } } else { const named = getExportsAssignName(t, left.node); if (named === null) return; exported.add(named); } } } else if (expr.isCallExpression()) { if (isObjEsModuleFlag(t, expr.node)) { state.set(HAS_ES_MODULE, true); } } }, }, VariableDeclaration(path) { if (path.node.declarations.length === 0) { path.remove(); } }, ConditionalExpression(path, state) { if (state.get(IS_ESM)) return; if ( t.isBinaryExpression(path.node.test) && t.isUnaryExpression(path.node.test.left) && path.node.test.left.operator === "typeof" && t.isIdentifier(path.node.test.left.argument) && path.node.test.left.argument.name === "exports" && path.node.test.operator === "===" ) { path.replaceWith(t.cloneNode(path.node.alternate, true)); } }, AssignmentExpression(path, state) { if (state.get(IS_ESM)) return; const exported = state.get(EXPORTED); const aliased = state.get(ALIASED); if (aliased === undefined) return; if ( path.node.operator === "=" && t.isMemberExpression(path.node.left) && t.isIdentifier(path.node.left.object) && path.node.left.object.name === aliased && t.isIdentifier(path.node.left.property) ) { const name = path.node.left.property.name; exported.add(name); } }, }, }; } function isModuleExports( t: typeof types, node: types.MemberExpression, ): boolean { return t.isIdentifier(node.object) && node.object.name === "module" && t.isIdentifier(node.property) && node.property.name === "exports"; } function getExportsAssignName( t: typeof types, node: types.MemberExpression, ): string | null { if ( (t.isMemberExpression(node.object) && isModuleExports(t, node.object) || t.isIdentifier(node.object) && node.object.name === "exports") && t.isIdentifier(node.property) ) { return node.property.name; } return null; } /** * Detect `exports.__esModule = true;` */ function isEsModuleFlag( t: typeof types, node: types.AssignmentExpression, ): boolean { if (!t.isMemberExpression(node.left)) return false; const { left, right } = node; return (t.isMemberExpression(left.object) && isModuleExports(t, left.object) || t.isIdentifier(left.object) && left.object.name === "exports") && t.isIdentifier(left.property) && left.property.name === "__esModule" && t.isBooleanLiteral(right); } /** * Check for `Object.defineProperty(exports, '__esModule', { value: true })` */ function isObjEsModuleFlag( t: typeof types, node: types.CallExpression, ): boolean { return node.arguments.length === 3 && t.isStringLiteral(node.arguments[1]) && node.arguments[1].value === "__esModule" && t.isObjectExpression(node.arguments[2]); } function isNodeBuiltin(specifier: string): boolean { return BUILTINS.has(specifier) || ( specifier.startsWith("node:") ? BUILTINS.has(specifier.slice("node:".length)) : BUILTINS.has(`node:${specifier}`) ); } ================================================ FILE: packages/plugin-vite/src/plugins/patches/commonjs_test.ts ================================================ import { expect } from "@std/expect/expect"; import * as babel from "@babel/core"; import { cjsPlugin } from "../patches/commonjs.ts"; function runTest( options: { input: string; expected: string; filename?: string }, ) { const res = babel.transformSync(options.input, { filename: options.filename ?? "foo.js", babelrc: false, plugins: [cjsPlugin], }); const output = res?.code ?? ""; expect(output).toEqual(options.expected); } const INIT = `var exports = {}, module = {}; Object.defineProperty(module, "exports", { get() { return exports; }, set(value) { exports = value; } }); Object.defineProperty(exports, "__esModule", { value: true });`; const DEFAULT_EXPORT = `const _default = exports.default ?? exports;`; const DEFAULT_EXPORT_END = `export default _default; export var __require = exports;`; const IMPORT_REQUIRE = `import { createRequire } from "node:module"; const require = createRequire(import.meta.url);`; const EXPORT_ES_MODULE = `export var __esModule = exports.__esModule;`; Deno.test("commonjs - module.exports default", () => { runTest({ input: `module.exports = async function () {};`, expected: `${INIT} module.exports = async function () {}; ${DEFAULT_EXPORT} ${DEFAULT_EXPORT_END}`, }); }); Deno.test("commonjs - module.exports default primitive", () => { runTest({ input: `module.exports = 42;`, expected: `${INIT} module.exports = 42; ${DEFAULT_EXPORT} ${DEFAULT_EXPORT_END}`, }); }); Deno.test("commonjs - exports with default + named", () => { runTest({ input: `exports.__esModule = true; exports.default = 'x'; exports.foo = 'foo';`, expected: `${INIT} exports.__esModule = true; exports.default = 'x'; exports.foo = 'foo'; var _foo = exports.foo; export { _foo as foo }; ${DEFAULT_EXPORT} ${DEFAULT_EXPORT_END} ${EXPORT_ES_MODULE}`, }); }); Deno.test("commonjs - module.exports with default + named", () => { runTest({ input: `module.exports.__esModule = true; module.exports.default = 'x'; module.exports.foo = 'foo';`, expected: `${INIT} module.exports.__esModule = true; module.exports.default = 'x'; module.exports.foo = 'foo'; var _foo = exports.foo; export { _foo as foo }; ${DEFAULT_EXPORT} ${DEFAULT_EXPORT_END} ${EXPORT_ES_MODULE}`, }); }); Deno.test("commonjs - Object es module flag with named clash", () => { runTest({ input: `Object.defineProperty(exports, '__esModule', { value: true }); exports.foo = 'bar'; const foo = 'also bar'; `, expected: `${INIT} Object.defineProperty(exports, '__esModule', { value: true }); exports.foo = 'bar'; const foo = 'also bar'; var _foo = exports.foo; export { _foo as foo }; ${DEFAULT_EXPORT} ${DEFAULT_EXPORT_END} ${EXPORT_ES_MODULE}`, }); }); Deno.test("commonjs - Object es module flag with named + default", () => { runTest({ input: `Object.defineProperty(exports, '__esModule', { value: true }); exports.default = 'foo'; exports.foo = 'bar'; `, expected: `${INIT} Object.defineProperty(exports, '__esModule', { value: true }); exports.default = 'foo'; exports.foo = 'bar'; var _foo = exports.foo; export { _foo as foo }; ${DEFAULT_EXPORT} ${DEFAULT_EXPORT_END} ${EXPORT_ES_MODULE}`, }); }); Deno.test("commonjs - esModule flag only", () => { runTest({ input: `Object.defineProperty(exports, "__esModule", { value: true });`, expected: `${INIT} Object.defineProperty(exports, "__esModule", { value: true }); const _default = exports.default ?? exports; export default _default; export var __require = exports; ${EXPORT_ES_MODULE}`, }); }); Deno.test("commonjs - esModule flag only #2", () => { runTest({ input: `Object.defineProperty(module.exports, "__esModule", { value: true });`, expected: `${INIT} Object.defineProperty(module.exports, "__esModule", { value: true }); const _default = exports.default ?? exports; export default _default; export var __require = exports; ${EXPORT_ES_MODULE}`, }); }); Deno.test("commonjs - esModule flag only minified #3", () => { runTest({ input: `Object.defineProperty(exports, '__esModule', { value: !0 });`, expected: `${INIT} Object.defineProperty(exports, '__esModule', { value: !0 }); const _default = exports.default ?? exports; export default _default; export var __require = exports; ${EXPORT_ES_MODULE}`, }); }); Deno.test("commonjs - exports only named", () => { runTest({ input: `Object.defineProperty(exports, '__esModule', { value: true }); exports.foo = 'bar'; exports.bar = 'foo'; `, expected: `${INIT} Object.defineProperty(exports, '__esModule', { value: true }); exports.foo = 'bar'; exports.bar = 'foo'; var _foo = exports.foo; var _bar = exports.bar; export { _foo as foo, _bar as bar }; ${DEFAULT_EXPORT} ${DEFAULT_EXPORT_END} ${EXPORT_ES_MODULE}`, }); }); Deno.test("commonjs - require", () => { runTest({ input: `var foo = require("tape"); console.log(foo); `, expected: `import * as _mod from "tape"; var foo = _mod.__require ?? _mod.default ?? _mod; console.log(foo);`, }); }); Deno.test("commonjs - require destructure", () => { runTest({ input: `var { foo } = require("tape"); console.log(foo); `, expected: `import * as _mod from "tape"; var { foo } = _mod; console.log(foo);`, }); }); Deno.test("commonjs - require assign", () => { runTest({ input: `foo = require("tape"); console.log(foo); `, expected: `import * as _mod from "tape"; foo = _mod; console.log(foo);`, }); }); Deno.test("commonjs - require assign pattern", () => { runTest({ input: `foo = require("tape"); console.log(foo); `, expected: `import * as _mod from "tape"; foo = _mod; console.log(foo);`, }); }); Deno.test("commonjs - require function call", () => { runTest({ input: `var a = require('./a')()`, expected: `import * as _mod from './a'; var a = (_mod.__require ?? _mod.default ?? _mod)();`, }); }); Deno.test("commonjs - require var decls", () => { runTest({ input: `var a = require('./a'), b = 42;`, expected: `import * as _mod from './a'; var a = _mod.__require ?? _mod.default ?? _mod, b = 42;`, }); }); Deno.test("commonjs - duplicate exports", () => { runTest({ input: `Object.defineProperty(exports, "__esModule", { value: true }); exports.trace = void 0; exports.trace = 'foo'`, expected: `${INIT} Object.defineProperty(exports, "__esModule", { value: true }); exports.trace = void 0; exports.trace = 'foo'; var _trace = exports.trace; export { _trace as trace }; ${DEFAULT_EXPORT} ${DEFAULT_EXPORT_END} ${EXPORT_ES_MODULE}`, }); }); Deno.test("commonjs - cleared exports", () => { runTest({ input: `Object.defineProperty(exports, "__esModule", { value: true }); exports.foo = exports.bar = void 0; exports.foo = 'foo'`, expected: `${INIT} Object.defineProperty(exports, "__esModule", { value: true }); exports.foo = 'foo'; var _foo = exports.foo; export { _foo as foo }; ${DEFAULT_EXPORT} ${DEFAULT_EXPORT_END} ${EXPORT_ES_MODULE}`, }); }); Deno.test("commonjs - define exports", () => { runTest({ input: `var utils_1 = require("./bar"); Object.defineProperty(exports, "foo", { enumerable: true, get: function () { return utils_1.foo; } });`, expected: `${INIT} import * as _mod from "./bar"; var utils_1 = _mod.__require ?? _mod.default ?? _mod; exports.foo = utils_1.foo; var _foo = exports.foo; export { _foo as foo }; ${DEFAULT_EXPORT} ${DEFAULT_EXPORT_END}`, }); }); Deno.test("commonjs - define exports #2", () => { runTest({ input: `var utils_1 = require("./bar"); Object.defineProperty(exports, "foo", { enumerable: true, get() { return utils_1.foo; } });`, expected: `${INIT} import * as _mod from "./bar"; var utils_1 = _mod.__require ?? _mod.default ?? _mod; exports.foo = utils_1.foo; var _foo = exports.foo; export { _foo as foo }; ${DEFAULT_EXPORT} ${DEFAULT_EXPORT_END}`, }); }); Deno.test("commonjs - define exports #3", () => { runTest({ input: `Object.defineProperty(exports, "__esModule", { value: true }); exports._globalThis = void 0; exports._globalThis = typeof globalThis === 'object' ? globalThis : global;`, expected: `${INIT} Object.defineProperty(exports, "__esModule", { value: true }); exports._globalThis = void 0; exports._globalThis = typeof globalThis === 'object' ? globalThis : global; var _globalThis = exports._globalThis; export { _globalThis }; ${DEFAULT_EXPORT} ${DEFAULT_EXPORT_END} ${EXPORT_ES_MODULE}`, }); }); Deno.test("commonjs - named function", () => { runTest({ input: `Object.defineProperty(exports, "__esModule", { value: true }); function foo() {}; exports.foo = foo;`, expected: `${INIT} Object.defineProperty(exports, "__esModule", { value: true }); function foo() {} exports.foo = foo; var _foo = exports.foo; export { _foo as foo }; ${DEFAULT_EXPORT} ${DEFAULT_EXPORT_END} ${EXPORT_ES_MODULE}`, }); }); Deno.test("commonjs - detect esbuild shims", () => { runTest({ input: `__exportStar(require("./globalThis"), exports);`, expected: `${INIT} import * as _ns from "./globalThis"; export * from "./globalThis"; ${DEFAULT_EXPORT} for (var _k in _ns) if (_k !== "default" && _k !== "__esModule" && Object.prototype.hasOwnProperty.call(_ns, _k)) _default[_k] = _ns[_k]; ${DEFAULT_EXPORT_END}`, }); }); Deno.test("commonjs - exports.default", () => { runTest({ input: `exports.default = {}`, expected: `${INIT} exports.default = {}; ${DEFAULT_EXPORT} ${DEFAULT_EXPORT_END}`, }); }); Deno.test("commonjs - multiple same name", () => { runTest({ input: `exports.VERSION = void 0; exports.VERSION = '1.9.0';`, expected: `${INIT} exports.VERSION = void 0; exports.VERSION = '1.9.0'; var _VERSION = exports.VERSION; export { _VERSION as VERSION }; ${DEFAULT_EXPORT} ${DEFAULT_EXPORT_END}`, }); }); Deno.test("commonjs - export enum", () => { runTest({ input: `Object.defineProperty(exports, "__esModule", { value: true }); exports.DiagLogLevel = void 0; var DiagLogLevel; (function (DiagLogLevel) { DiagLogLevel[DiagLogLevel["ALL"] = 9999] = "ALL"; })(DiagLogLevel = exports.DiagLogLevel || (exports.DiagLogLevel = {}));`, expected: `${INIT} Object.defineProperty(exports, "__esModule", { value: true }); exports.DiagLogLevel = void 0; var DiagLogLevel; (function (DiagLogLevel) { DiagLogLevel[DiagLogLevel["ALL"] = 9999] = "ALL"; })(DiagLogLevel = exports.DiagLogLevel || (exports.DiagLogLevel = {})); var _DiagLogLevel = exports.DiagLogLevel; export { _DiagLogLevel as DiagLogLevel }; ${DEFAULT_EXPORT} ${DEFAULT_EXPORT_END} ${EXPORT_ES_MODULE}`, }); }); Deno.test("commonjs - require", () => { runTest({ input: `module.exports = { __esModule: true, default: { foo: 'bar' }}`, expected: `${INIT} module.exports = { __esModule: true, default: { foo: 'bar' } }; ${DEFAULT_EXPORT} ${DEFAULT_EXPORT_END}`, }); }); Deno.test("commonjs - export default object", () => { runTest({ input: `Object.defineProperty(exports, '__esModule', { value: true }); module.exports = { foo: 'bar' }; `, expected: `${INIT} Object.defineProperty(exports, '__esModule', { value: true }); module.exports = { foo: 'bar' }; var _foo = exports.foo; export { _foo as foo }; ${DEFAULT_EXPORT} ${DEFAULT_EXPORT_END} ${EXPORT_ES_MODULE}`, }); }); Deno.test("commonjs - detect iife wrapper", () => { runTest({ input: `;(function (sax) { sax.foo = "foo"; })(typeof exports === 'undefined' ? this.sax = {} : exports);`, expected: `${INIT} (function (sax) { sax.foo = "foo"; })(exports); var _foo = exports.foo; export { _foo as foo }; ${DEFAULT_EXPORT} ${DEFAULT_EXPORT_END}`, }); }); Deno.test("commonjs - re-export", () => { runTest({ input: `;var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __exportStar = (this && this.__exportStar) || function(m, exports) { for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); }; Object.defineProperty(exports, "__esModule", { value: true }); __exportStar(require("./node"), exports);`, expected: `${INIT} import * as _ns from "./node"; var __createBinding = this && this.__createBinding || (Object.create ? function (o, m, k, k2) { if (k2 === undefined) k2 = k; Object.defineProperty(o, k2, { enumerable: true, get: function () { return m[k]; } }); } : function (o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; }); var __exportStar = this && this.__exportStar || function (m, exports) { for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); }; Object.defineProperty(exports, "__esModule", { value: true }); export * from "./node"; ${DEFAULT_EXPORT} for (var _k in _ns) if (_k !== "default" && _k !== "__esModule" && Object.prototype.hasOwnProperty.call(_ns, _k)) _default[_k] = _ns[_k]; ${DEFAULT_EXPORT_END} ${EXPORT_ES_MODULE}`, }); }); Deno.test("commonjs - assign module.exports", () => { runTest({ input: `module.exports = { foo: 1 };`, expected: `${INIT} module.exports = { foo: 1 }; var _foo = exports.foo; export { _foo as foo }; ${DEFAULT_EXPORT} ${DEFAULT_EXPORT_END}`, }); }); Deno.test("commonjs - require non-analyzable arg", () => { runTest({ input: `const pkg = require(path.join(basedir, "package.json"))`, expected: `import { createRequire } from "node:module"; const require = createRequire(import.meta.url); const pkg = require(path.join(basedir, "package.json"));`, }); }); Deno.test("commonjs - keep binding", () => { runTest({ input: `export var __createBinding = Object.create ? 1 : 2;`, expected: `export var __createBinding = Object.create ? 1 : 2;`, }); }); Deno.test("commonjs - require lazy import", () => { runTest({ input: `if (typeof process.env.NODE_PG_FORCE_NATIVE !== 'undefined') { module.exports = new PG(require('./native')) } else { module.exports = new PG(Client) // lazy require native module...the native module may not have installed Object.defineProperty(module.exports, 'native', { configurable: true, enumerable: false, get() { let native = null try { native = new PG(require('./native')) } catch (err) { if (err.code !== 'MODULE_NOT_FOUND') { throw err } } // overwrite module.exports.native so that getter is never called again Object.defineProperty(module.exports, 'native', { value: native, }) return native }, }) }`, expected: `${INIT} ${IMPORT_REQUIRE} if (typeof process.env.NODE_PG_FORCE_NATIVE !== 'undefined') { module.exports = new PG(require('./native')); } else { module.exports = new PG(Client); // lazy require native module...the native module may not have installed Object.defineProperty(module.exports, 'native', { configurable: true, enumerable: false, get() { let native = null; try { native = new PG(require('./native')); } catch (err) { if (err.code !== 'MODULE_NOT_FOUND') { throw err; } } // overwrite module.exports.native so that getter is never called again Object.defineProperty(module.exports, 'native', { value: native }); return native; } }); } ${DEFAULT_EXPORT} ${DEFAULT_EXPORT_END}`, }); }); Deno.test("commonjs - wrapped iife binding", () => { runTest({ input: `"production" !== process.env.NODE_ENV && (function() { exports.foo = 123 })()`, expected: `${INIT} "production" !== process.env.NODE_ENV && function () { exports.foo = 123; }(); var _foo = exports.foo; export { _foo as foo }; ${DEFAULT_EXPORT} ${DEFAULT_EXPORT_END}`, }); }); Deno.test("commonjs - re-export #2", () => { runTest({ input: `module.exports = require("foo");`, expected: `${INIT} export * from "foo"; import * as _mod from "foo"; module.exports = _mod; ${DEFAULT_EXPORT} ${DEFAULT_EXPORT_END}`, }); }); Deno.test("commonjs - keep require() in .mjs", () => { runTest({ filename: "foo.mjs", input: `try { require("foo"); } catch {}`, expected: `try { require("foo"); } catch {}`, }); }); Deno.test("commonjs - keep conditional require() in ESM file", () => { runTest({ filename: "foo.mjs", input: `try { require("foo"); } catch {}; export {};`, expected: `try { require("foo"); } catch {} export {};`, }); }); Deno.test("commonjs - CJS turned ESM module", () => { runTest({ filename: "foo.mjs", input: `module.exports.create = confettiCannon; export default module.exports; export var create = module.exports.create;`, expected: `module.exports.create = confettiCannon; export default module.exports; export var create = module.exports.create;`, }); }); Deno.test("commonjs - minified __esModule", () => { runTest({ filename: "foo.js", input: ` const m = module.exports; const a = Object.defineProperty; a(m, "__esModule", { value: !0 });`, expected: `${INIT} const m = module.exports; const a = Object.defineProperty; a(m, "__esModule", { value: !0 }); ${DEFAULT_EXPORT} ${DEFAULT_EXPORT_END} export var __esModule = exports.__esModule;`, }); }); Deno.test("commonjs - esbuild __importDefault", () => { runTest({ input: `var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; const node_events_1 = __importDefault(require("node:events"));`, expected: `import * as _mod from "node:events"; var __importDefault = this && this.__importDefault || function (mod) { return mod && mod.__esModule ? mod : { "default": mod }; }; const node_events_1 = __importDefault({ __esModule: true, default: _mod.default ?? _mod });`, }); }); ================================================ FILE: packages/plugin-vite/src/plugins/patches/http_absolute.ts ================================================ import type { NodePath, PluginObj, PluginPass, types, Visitor, } from "@babel/core"; function maybeRewrite( t: typeof types, path: NodePath, source: string, url: URL, ) { if (source.startsWith("/")) { const s = `deno-http::${url.origin}${source}`; path.replaceWith(t.stringLiteral(s)); } else if (/^https?:\/\//.test(source)) { const s = `deno-http::${source}`; path.replaceWith(t.stringLiteral(s)); } } export function httpAbsolute(url: URL | null) { return ({ types: t }: { types: typeof types }): PluginObj => { const visitor: Visitor = url !== null ? { ImportDeclaration(path) { const source = path.node.source.value; maybeRewrite(t, path.get("source"), source, url); }, ExportAllDeclaration(path) { const source = path.node.source.value; maybeRewrite(t, path.get("source"), source, url); }, ExportNamedDeclaration(path) { if (!path.node.source) return; const source = path.node.source.value; // deno-lint-ignore no-explicit-any maybeRewrite(t, path.get("source") as any, source, url); }, CallExpression(path) { if ( t.isImport(path.node.callee) && path.node.arguments.length > 0 && t.isStringLiteral(path.node.arguments[0]) ) { const source = path.node.arguments[0].value; maybeRewrite( t, path .get("arguments")[0], source, url, ); } }, } : {}; return { name: "fresh-http-absolute", visitor, }; }; } ================================================ FILE: packages/plugin-vite/src/plugins/patches/http_absolute_test.ts ================================================ import { expect } from "@std/expect/expect"; import * as babel from "@babel/core"; import { httpAbsolute } from "./http_absolute.ts"; function runTest(options: { input: string; expected: string; url?: URL }) { const res = babel.transformSync(options.input, { filename: "foo.js", babelrc: false, plugins: [httpAbsolute(options.url ?? null)], }); const output = res?.code ?? ""; expect(output).toEqual(options.expected); } Deno.test("http absolute - change import sources", () => { runTest({ input: `import foo from "/foo.js"`, expected: `import foo from "deno-http::http://localhost/foo.js";`, url: new URL("http://localhost"), }); }); Deno.test("http absolute - change export all sources", () => { runTest({ input: `export * as foo from "/foo.js"`, expected: `export * as foo from "deno-http::http://localhost/foo.js";`, url: new URL("http://localhost"), }); }); Deno.test("http absolute - change export sources", () => { runTest({ input: `export { foo } from "/foo.js"`, expected: `export { foo } from "deno-http::http://localhost/foo.js";`, url: new URL("http://localhost"), }); }); Deno.test("http absolute - change import()", () => { runTest({ input: `import("/foo.js")`, expected: `import("deno-http::http://localhost/foo.js");`, url: new URL("http://localhost"), }); }); ================================================ FILE: packages/plugin-vite/src/plugins/patches/inline_env_vars.ts ================================================ import type { NodePath, PluginObj, types } from "@babel/core"; export function inlineEnvVarsPlugin(mode: string, env: Record) { const allowed = new Map(); for (const [name, value] of Object.entries(env)) { if (name.startsWith("FRESH_PUBLIC_")) { allowed.set(name, value); } } allowed.set("NODE_ENV", mode); return ( { types: t }: { types: typeof types }, ): PluginObj => { function replace(path: NodePath, name: string) { if (allowed.has(name)) { const value = allowed.get(name); if (value !== undefined) { path.replaceWith(t.stringLiteral(value)); } else { path.replaceWith(t.identifier("undefined")); } } } return { name: "fresh-env-var", visitor: { MemberExpression(path) { // Check: process.env.* if ( t.isMemberExpression(path.node.object) && t.isIdentifier(path.node.object.object) && path.node.object.object.name === "process" && t.isIdentifier(path.node.object.property) && path.node.object.property.name === "env" && t.isIdentifier(path.node.property) ) { const name = path.node.property.name; replace(path, name); } // Check: import.meta.env.* if ( t.isIdentifier(path.node.property) && t.isMemberExpression(path.node.object) && t.isIdentifier(path.node.object.property) && path.node.object.property.name === "env" && t.isMetaProperty(path.node.object.object) ) { const name = path.node.property.name; replace(path, name); } }, CallExpression(path) { // Check: Deno.env.get("") if ( t.isMemberExpression(path.node.callee) && t.isMemberExpression(path.node.callee.object) && t.isIdentifier(path.node.callee.object.object) && path.node.callee.object.object.name === "Deno" && t.isIdentifier(path.node.callee.object.property) && path.node.callee.object.property.name === "env" && t.isIdentifier(path.node.callee.property) && path.node.callee.property.name === "get" && path.node.arguments.length > 0 && t.isStringLiteral(path.node.arguments[0]) ) { const name = path.node.arguments[0].value; replace(path, name); } }, }, }; }; } ================================================ FILE: packages/plugin-vite/src/plugins/patches/inline_env_vars_test.ts ================================================ import { expect } from "@std/expect/expect"; import * as babel from "@babel/core"; import { inlineEnvVarsPlugin } from "./inline_env_vars.ts"; function runTest( options: { input: string; expected: string; mode?: string; env?: Record; }, ) { const res = babel.transformSync(options.input, { filename: "foo.js", babelrc: false, plugins: [ inlineEnvVarsPlugin(options.mode ?? "development", options.env ?? {}), ], }); const output = res?.code ?? ""; expect(output).toEqual(options.expected); } Deno.test("env vars - inline NODE_ENV mode", () => { runTest({ input: `() => process.env.NODE_ENV`, expected: `() => "asdf";`, mode: "asdf", }); }); Deno.test("env vars - inline custom process.env.*", () => { runTest({ input: `() => process.env.FRESH_PUBLIC_FOO`, expected: `() => "a";`, env: { FRESH_PUBLIC_FOO: "a", }, }); }); Deno.test("env vars - inline Deno.env.get()", () => { runTest({ input: `() => Deno.env.get("FRESH_PUBLIC_FOO")`, expected: `() => "b";`, env: { FRESH_PUBLIC_FOO: "b", }, }); }); Deno.test("env vars - inline Deno.env.get(NODE_ENV)", () => { runTest({ input: `() => Deno.env.get("NODE_ENV")`, expected: `() => "c";`, mode: "c", }); }); Deno.test("env vars - inline const _ = Deno.env.get()", () => { runTest({ input: `const deno = Deno.env.get("FRESH_PUBLIC_FOO");`, expected: `const deno = "test";`, env: { FRESH_PUBLIC_FOO: "test", }, }); }); Deno.test("env vars - inline import.meta.env.FRESH_PUBLIC_FOO", () => { runTest({ input: `() => import.meta.env.FRESH_PUBLIC_FOO;`, expected: `() => "test";`, env: { FRESH_PUBLIC_FOO: "test", }, }); }); ================================================ FILE: packages/plugin-vite/src/plugins/patches/jsx_comment.ts ================================================ import type { PluginObj, types } from "@babel/core"; export function jsxComments(): PluginObj { return { name: "fresh-jsx-comment", visitor: { Program(path) { const comments = (path.parent as types.File).comments; if (comments) { for (let i = 0; i < comments.length; i++) { const comment = comments[i]; comment.value = comment.value.replaceAll(/@jsx\w+\s+[^\s*]+/g, ""); } } }, }, }; } ================================================ FILE: packages/plugin-vite/src/plugins/patches/jsx_comment_test.ts ================================================ import { expect } from "@std/expect/expect"; import * as babel from "@babel/core"; import { jsxComments } from "./jsx_comment.ts"; function runTest(options: { input: string; expected: string }) { const res = babel.transformSync(options.input, { filename: "foo.js", babelrc: false, plugins: [jsxComments], }); const output = res?.code ?? ""; expect(output).toEqual(options.expected); } Deno.test("jsx comments - import declaration", () => { runTest({ input: `/** @jsxRuntime classic */ /** @jsxImportSource npm:preact@^10.27.0 */ /** @jsxImportSourceTypes npm:preact@^10.27.0 */ /** @jsxFactory React.createElement */ /** @jsxFragmentFactory React.Fragment */ foo;`, expected: `/** */ /** */ /** */ /** */ /** */foo;`, }); }); ================================================ FILE: packages/plugin-vite/src/plugins/patches/remove_polyfills.ts ================================================ import type { PluginObj, types } from "@babel/core"; export function removePolyfills( { types: t }: { types: typeof types }, ): PluginObj { return { name: "fresh-remove-polyfills", visitor: { IfStatement(path) { if ( t.isUnaryExpression(path.node.test) && path.node.test.operator === "!" && t.isMemberExpression(path.node.test.argument) && t.isIdentifier(path.node.test.argument.object) && ((path.node.test.argument.object.name === "String" && t.isIdentifier(path.node.test.argument.property) && path.node.test.argument.property.name === "fromCodePoint") || (path.node.test.argument.object.name === "Object" && t.isIdentifier(path.node.test.argument.property) && (path.node.test.argument.property.name === "keys" || path.node.test.argument.property.name === "create"))) ) { if (path.node.alternate) { path.replaceWith(t.cloneNode(path.node.alternate, true)); } else { path.remove(); } } }, }, }; } ================================================ FILE: packages/plugin-vite/src/plugins/patches/remove_polyfills_test.ts ================================================ import { expect } from "@std/expect/expect"; import * as babel from "@babel/core"; import { removePolyfills } from "./remove_polyfills.ts"; function runTest(options: { input: string; expected: string }) { const res = babel.transformSync(options.input, { filename: "foo.js", babelrc: false, plugins: [removePolyfills], }); const output = res?.code ?? ""; expect(output).toEqual(options.expected); } Deno.test("remove polyfills - Object.keys", () => { runTest({ input: `if (!Object.keys) { Object.keys = function (o) { var a = [] for (var i in o) if (o.hasOwnProperty(i)) a.push(i) return a } } Object.keys({ foo: "bar" });`, expected: `Object.keys({ foo: "bar" });`, }); }); Deno.test("remove polyfills - Object.create", () => { runTest({ input: `if (!Object.create) { Object.create = function (o) { function F () {} F.prototype = o var newf = new F() return newf } } Object.create({});`, expected: `Object.create({});`, }); }); Deno.test("remove polyfills - keep alternate", () => { runTest({ input: `if (!Object.create) { Object.create = function (o) { function F () {} F.prototype = o var newf = new F() return newf } } else { console.log("foo") }`, expected: `{ console.log("foo"); }`, }); }); Deno.test("remove polyfills - String.fromCodePoint", () => { runTest({ input: `if (!String.fromCodePoint) { foo } String.fromCodePoint(42);`, expected: `String.fromCodePoint(42);`, }); }); Deno.test("remove polyfills - Keep unrelated unary statements", () => { runTest({ input: `if (+String.fromCodePoint) { foo; }`, expected: `if (+String.fromCodePoint) { foo; }`, }); }); ================================================ FILE: packages/plugin-vite/src/plugins/patches.ts ================================================ import type { Plugin } from "vite"; import * as babel from "@babel/core"; import { cjsPlugin } from "./patches/commonjs.ts"; import { jsxComments } from "./patches/jsx_comment.ts"; import { inlineEnvVarsPlugin } from "./patches/inline_env_vars.ts"; import { removePolyfills } from "./patches/remove_polyfills.ts"; import { JS_REG, JSX_REG } from "../utils.ts"; import { codeEvalPlugin } from "./patches/code_eval.ts"; // @ts-ignore Workaround for https://github.com/denoland/deno/issues/30850 const { default: babelReact } = await import("@babel/preset-react"); export function patches(): Plugin { let isDev = false; return { name: "fresh:patches", sharedDuringBuild: true, config(_, env) { isDev = env.command === "serve"; }, applyToEnvironment() { return true; }, transform: { filter: { id: JS_REG, }, handler(code, id) { const presets = []; if (this.environment.config.consumer === "client" && JSX_REG.test(id)) { presets.push([babelReact, { runtime: "automatic", importSource: "preact", development: isDev, throwIfNamespace: false, }]); } const env = isDev ? "development" : "production"; const plugins: babel.PluginItem[] = [ codeEvalPlugin(this.environment.config.consumer, env), cjsPlugin, removePolyfills, jsxComments, inlineEnvVarsPlugin(env, Deno.env.toObject()), ]; const res = babel.transformSync(code, { filename: id, babelrc: false, compact: false, plugins, presets, sourceMaps: "both", }); if (res?.code) { return { code: res.code, map: res.map, }; } }, }, }; } ================================================ FILE: packages/plugin-vite/src/plugins/server_entry.ts ================================================ import type { Manifest, Plugin } from "vite"; import { generateServerEntry, type PendingStaticFile, prepareStaticFile, writeCompiledEntry, } from "fresh/internal-dev"; import { pathWithRoot, type ResolvedFreshViteConfig } from "../utils.ts"; import * as path from "@std/path"; export function serverEntryPlugin( options: ResolvedFreshViteConfig, ): Plugin { const modName = "fresh:server_entry"; let serverEntry = ""; let serverOutDir = ""; let clientOutDir = ""; let root = ""; let basePath = ""; let isDev = false; const getAssetPath = (id: string): string => { if (basePath === "/") { return `/${id}`; } // Ensure basePath ends with / and construct the path manually to avoid platform-specific path issues const normalizedBase = basePath.endsWith("/") ? basePath : basePath + "/"; return normalizedBase + id; }; return { name: "fresh:server_entry", sharedDuringBuild: true, applyToEnvironment(env) { return env.config.consumer === "server"; }, config(_, env) { isDev = env.command === "serve"; }, configResolved(config) { root = config.root; basePath = config.base || "/"; if (basePath !== "/" && !basePath.endsWith("/")) { basePath += "/"; } serverEntry = pathWithRoot(options.serverEntry, config.root); serverOutDir = pathWithRoot( config.environments.ssr.build.outDir, config.root, ); clientOutDir = pathWithRoot( config.environments.client.build.outDir, config.root, ); }, resolveId: { filter: { id: /fresh:server_entry/, }, handler(id) { if (id === modName) { return `\0${modName}`; } }, }, load: { filter: { id: /\0fresh:server_entry/, }, handler() { let code = generateServerEntry({ root: isDev ? path.relative(serverOutDir, root) : "..", serverEntry: path.toFileUrl(serverEntry).href, snapshotSpecifier: "fresh:server-snapshot", }); code += ` export function registerStaticFile(prepared) { snapshot.staticFiles.set(prepared.name, { name: prepared.name, contentType: prepared.contentType, filePath: prepared.filePath }); } `; if (isDev) { code = `import "preact/debug"; import { setErrorInterceptor as internalErrorIntercept } from "fresh/internal"; ${code} export function setErrorInterceptor(fn) { internalErrorIntercept(app, fn); } if (import.meta.hot) import.meta.hot.accept();`; } return code; }, }, async writeBundle(_options, bundle) { const manifest = bundle[".vite/manifest.json"]; const staticFiles: PendingStaticFile[] = []; if ( manifest && manifest.type === "asset" && typeof manifest.source === "string" ) { const json = JSON.parse(manifest.source) as Manifest; for (const item of Object.values(json)) { if (item.assets) { for (let i = 0; i < item.assets.length; i++) { const id = item.assets[i]; staticFiles.push({ filePath: path.join(serverOutDir, id), hash: null, pathname: getAssetPath(id), }); } } if (item.css) { for (let i = 0; i < item.css.length; i++) { const id = item.css[i]; staticFiles.push({ filePath: path.join(serverOutDir, id), hash: null, pathname: getAssetPath(id), }); } } } } const registered = await Promise.all(staticFiles.map(async (file) => { const prepared = await prepareStaticFile(file, serverOutDir); const rel = path.relative(serverOutDir, file.filePath); const target = path.join(clientOutDir, rel); await Deno.rename(file.filePath, target); prepared.filePath = path.join("client", prepared.filePath); return `registerStaticFile(${JSON.stringify(prepared)});`; })); const outDir = path.dirname(serverOutDir); await Deno.writeTextFile( path.join(outDir, "server.js"), `import server, { registerStaticFile } from "./server/server-entry.mjs"; ${registered.join("\n")} export default { fetch: server.fetch }; `, ); await writeCompiledEntry(outDir); }, }; } ================================================ FILE: packages/plugin-vite/src/plugins/server_snapshot.ts ================================================ import type { EnvironmentModuleNode, Manifest, Plugin, ViteDevServer, } from "vite"; import { crawlFsItem, fsAdapter, type FsRouteFileNoMod, generateSnapshotServer, type IslandModChunk, pathToSpec, type PendingStaticFile, specToName, UniqueNamer, } from "fresh/internal-dev"; import { JS_REG, pathWithRoot, type ResolvedFreshViteConfig, } from "../utils.ts"; import * as path from "@std/path"; import { getBuildId } from "./build_id.ts"; export function serverSnapshot(options: ResolvedFreshViteConfig): Plugin[] { const modName = "fresh:server-snapshot"; let isDev = false; let server: ViteDevServer | undefined; let clientOutDir = ""; let serverOutDir = ""; let root = ""; let publicDir = ""; const islands = new Map(); const islandsByFile = new Set(); const islandSpecByName = new Map(); const routeNamer = new UniqueNamer(); const routeFileToName = new Map(); // deno-lint-ignore no-explicit-any const routes: Map> = new Map(); return [ { name: "fresh:server-snapshot", sharedDuringBuild: true, applyToEnvironment(env) { return env.config.consumer === "server"; }, config(_, env) { isDev = env.command === "serve"; }, configResolved(config) { root = config.root; publicDir = pathWithRoot(config.publicDir, config.root); clientOutDir = pathWithRoot( config.environments.client.build.outDir, config.root, ); serverOutDir = pathWithRoot( config.environments.ssr.build.outDir, config.root, ); options.islandSpecifiers.forEach((name, spec) => { islands.set(spec, { name, chunk: null }); islandSpecByName.set(name, spec); // islandsByFile.add(spec); }); }, configureServer(viteServer) { server = viteServer; viteServer.watcher.on("all", (ev, filePath) => { const { client, ssr } = viteServer.environments; // We can't just check if it's in the client module graph // because plugins like tailwindcss import _everything_. // Instead, we walk up the module graph to see if the file // is imported by an island or the client entry file. const mods = client.moduleGraph.getModulesByFile(filePath); if (mods !== undefined) { const seen = new Set(); const maybe = Array.from(mods); for (let i = 0; i < maybe.length; i++) { const mod = maybe[i]; const isIslandFile = walkUp( mod, (m) => m.file !== null && islandsByFile.has(m.file), seen, ); // No need to notify manually, vite takes care of this. if (isIslandFile) return; } } // Check for route files. We need to invalidate the snapshot if // they are removed or added. if ( (ev === "add" || ev === "unlink") && !/[\\/]+\(_[^)]+\)[\\/]+/.test(filePath) ) { const relRoutes = path.relative(options.routeDir, filePath); if (!relRoutes.startsWith("..")) { const mod = ssr.moduleGraph.getModuleById(`\0${modName}`); if (mod !== undefined) { // Clear state islands.clear(); islandsByFile.clear(); islandSpecByName.clear(); ssr.moduleGraph.invalidateModule(mod); } } } // Finally, notify the client viteServer.ws.send("fresh:reload"); }); }, resolveId: { filter: { id: /fresh:server-snapshot/, }, handler(id) { if (id === modName) { return `\0${modName}`; } }, }, load: { filter: { id: /\0fresh:server-snapshot/, }, async handler() { const result = await crawlFsItem({ islandDir: options.islandsDir, routeDir: options.routeDir, ignore: options.ignore, }); for (let i = 0; i < result.islands.length; i++) { const spec = result.islands[i]; const specName = specToName(spec); const name = options.namer.getUniqueName(specName); islands.set(spec, { name, chunk: null }); islandSpecByName.set(name, spec); islandsByFile.add(spec); } for (let i = 0; i < result.routes.length; i++) { const route = result.routes[i]; const name = routeNamer.getUniqueName(route.id); routeFileToName.set(route.filePath, name); routes.set(name, route); } const staticFiles: PendingStaticFile[] = []; let islandMods: IslandModChunk[] = []; let clientEntry = "/@id/fresh:client-entry"; let buildId = ""; const entryAssets: string[] = []; if (isDev && server !== undefined) { for (const id of islands.keys()) { const mod = server.environments.client.moduleGraph.getModuleById( id, ); if (mod !== undefined) { const def = islands.get(id); if (def !== undefined) def.chunk = mod.url; } } islandMods = Array.from(islands.entries()).map(([id, def]) => { return { name: def.name, server: id, browser: def.chunk ?? `/@id/fresh-island::${def.name}`, css: [], }; }); } else { buildId = await getBuildId(false); const manifest = JSON.parse( await Deno.readTextFile( path.join(clientOutDir, ".vite", "manifest.json"), ), ) as Manifest; const resolvedIslandSpecs = new Map(); for (const spec of options.islandSpecifiers.keys()) { const resolved = await this.resolve(spec); if (resolved === null) continue; const id = resolved.id.startsWith("\0") ? resolved.id.slice(1) : resolved.id; resolvedIslandSpecs.set(id, spec); } const clientEntryName = "client-entry"; for (const chunk of Object.values(manifest)) { if (chunk.name === clientEntryName) { clientEntry = pathToSpec(clientOutDir, chunk.file); } staticFiles.push({ filePath: path.join(clientOutDir, chunk.file), pathname: chunk.file, hash: null, }); if (chunk.css !== undefined) { for (let i = 0; i < chunk.css.length; i++) { const id = chunk.css[i]; const pathname = `/${id}`; staticFiles.push({ filePath: path.join(clientOutDir, id), hash: null, pathname, }); if (chunk.name === clientEntryName) { entryAssets.push(pathname); } } } if (chunk.name?.startsWith("fresh-island__")) { const name = chunk.name.slice("fresh-island__".length); let serverPath = path.join(root, chunk.src ?? chunk.file); const idx = chunk.src?.indexOf("deno::") ?? -1; if (idx > -1 && chunk.src) { const src = chunk.src .slice(idx) .replace( /(https?):\/([^/])/, (_m, protocol, rest) => { return `${protocol}://${rest}`; }, ); serverPath = resolvedIslandSpecs.get(src)!; } let spec = pathToSpec(clientOutDir, chunk.file); if (spec.startsWith("./")) { spec = spec.slice(1); } const chunkCss = chunk.css?.map((id) => `/${id}`) ?? []; islandMods.push({ name, browser: spec, server: serverPath, css: chunkCss, }); } } if (await fsAdapter.isDirectory(publicDir)) { const entries = await fsAdapter.walk( publicDir, { followSymlinks: false, includeDirs: false, includeFiles: true, skip: options.ignore, }, ); for await (const entry of entries) { const relative = path.relative(publicDir, entry.path); const filePath = path.join(clientOutDir, relative); try { await Deno.mkdir(path.dirname(filePath), { recursive: true }); } catch (err) { if (!(err instanceof Deno.errors.AlreadyExists)) { throw err; } } await Deno.copyFile(entry.path, filePath); staticFiles.push({ filePath, hash: null, pathname: relative, }); if (path.basename(relative) === "index.html") { const htmlRelative = path.relative( publicDir, path.dirname(entry.path), ); staticFiles.push({ filePath, hash: null, pathname: htmlRelative, }); } } } } const code = await generateSnapshotServer({ outDir: path.join(clientOutDir, ".."), staticFiles, buildId, clientEntry, entryAssets, fsRoutesFiles: result.routes, islands: islandMods, writeSpecifier: (file) => { const def = islands.get(file); if (def) { return `fresh-island::${def.name}`; } const routeDef = routeFileToName.get(file); if (routeDef !== undefined) { return `fresh-route::${routeDef}`; } return path.toFileUrl(file).href; }, }); return code; }, }, transform: { filter: { id: /\.(css|less|sass|scss)(\?.*)?$/, }, handler(_code, id) { if (server) { const ssrGraph = server.environments.ssr.moduleGraph; const mod = ssrGraph.getModuleById(id); if (mod === undefined) return; const snapshot = ssrGraph.getModuleById("\0fresh:server-snapshot"); if (snapshot === undefined) return; const queue: EnvironmentModuleNode[] = [mod]; let item: EnvironmentModuleNode | undefined; while ((item = queue.pop()) !== undefined) { if (item.file !== null) { const normalized = path.normalize(item.file); const name = routeFileToName.get(normalized); if (name !== undefined) { const route = routes.get(name); if (route !== undefined) { const mod = ssrGraph.getModuleById(id); if (mod !== undefined) { route.css.push(mod.url); } const routeMod = ssrGraph.getModuleById( `\0fresh-route-css::${name}`, ); if (routeMod !== undefined) { ssrGraph.invalidateModule(routeMod); } } } } item.importers.forEach((importer) => queue.push(importer)); } } }, }, }, { name: "fresh:island-resolver", sharedDuringBuild: true, resolveId: { filter: { id: /^fresh-island::.*/, }, handler(id) { let name = id.slice("fresh-island::".length); if (JS_REG.test(name)) { name = name.slice(0, name.lastIndexOf(".")); } const spec = islandSpecByName.get(name); if (spec !== undefined) return spec; }, }, }, { name: "fresh:route-css", sharedDuringBuild: true, resolveId: { filter: { id: /^(\/@id\/)?fresh-route-css::/, }, handler(id) { let name = id.startsWith("/@id/") ? id.slice("/@id/fresh-route-css::".length) : id.slice("fresh-route-css::".length); const idx = name.indexOf(".module."); if (idx > -1) { name = name.slice(0, idx); } return `\0fresh-route-css::${name}`; }, }, load: { filter: { id: /^\0fresh-route-css::/, }, handler(id) { const name = id.slice("\0fresh-route-css::".length); const route = routes.get(name); if (route === undefined) return; if (!isDev) { return `export default ["__FRESH_CSS_PLACEHOLDER__"];`; } const imports = route.css.map((css) => `import "${css}";`).join("\n"); return `${imports} export default ${JSON.stringify(route.css)} `; }, }, }, { name: "fresh-route-css-build-ssr", sharedDuringBuild: true, applyToEnvironment(env) { return env.config.consumer === "server"; }, async writeBundle(_, bundle) { const asset = bundle[".vite/manifest.json"]; if (asset.type === "asset") { const manifest = JSON.parse(asset.source as string) as Manifest; for (const info of Object.values(manifest)) { if (info.name?.startsWith("_fresh-route___")) { const filePath = path.join(serverOutDir, info.file); const content = await Deno.readTextFile(filePath); const replaced = content.replace( `["__FRESH_CSS_PLACEHOLDER__"]`, info.css ? JSON.stringify(info.css.map((css) => `/${css}`)) : "null", ); await Deno.writeTextFile(filePath, replaced); } } } }, }, { name: "fresh:route-resolver", sharedDuringBuild: true, resolveId: { filter: { id: /^fresh-route::/, }, handler(id) { let name = id.slice("fresh-route::".length); if (JS_REG.test(name)) { name = name.slice(0, name.lastIndexOf(".")); } return `\0fresh-route::${name}`; }, }, load: { filter: { id: /^\0fresh-route::.*/, }, handler(id) { const name = id.slice("\0fresh-route::".length); const route = routes.get(name); if (route === undefined) return; const fileUrl = path.toFileUrl(route.filePath).href; const cssId = isDev ? `/@id/fresh-route-css::${name}.module.css` : `fresh-route-css::${name}.module.css`; // For some reason doing `export * from "foo"` is broken // in vite. const code = `import * as mod from "${fileUrl}"; import routeCss from "${cssId}"; export const css = routeCss; export const config = mod.config; export const handler = mod.handler; export const handlers = mod.handlers; export default mod.default; `; return { code }; }, }, }, ]; } function walkUp( mod: EnvironmentModuleNode, fn: (mod: EnvironmentModuleNode) => boolean, seen: Set, ): boolean { if (seen.has(mod)) return false; if (fn(mod)) return true; const importers = Array.from(mod.importers); for (let i = 0; i < importers.length; i++) { const imp = importers[i]; if (walkUp(imp, fn, seen)) return true; } return false; } ================================================ FILE: packages/plugin-vite/src/plugins/shims/object.entries/index.ts ================================================ // deno-lint-ignore no-explicit-any export default function entries(obj: any) { return Object.entries(obj); } ================================================ FILE: packages/plugin-vite/src/plugins/shims/supports-color/index.ts ================================================ export interface ColorSupport { level: 1 | 2 | 3; hasBasic: boolean; has256: boolean; has16m: boolean; } function toLevel(depth: number): 1 | 2 | 3 { if (depth === 1) return 1; if (depth === 8) return 2; if (depth === 24) return 3; return 1; } function toSupport(depth: number): ColorSupport { const level = toLevel(depth); return { level, hasBasic: true, has256: level >= 2, has16m: level >= 3, }; } export default { // deno-lint-ignore no-process-global stdout: toSupport(process.stdout.getColorDepth()), // deno-lint-ignore no-process-global stderr: toSupport(process.stderr.getColorDepth()), }; ================================================ FILE: packages/plugin-vite/src/plugins/shims.ts ================================================ import type { Plugin } from "vite"; import * as path from "@std/path"; const SHIMS: Record = { "object.entries": path.join( import.meta.dirname!, "shims", "object.entries", "index.ts", ), "supports-color": path.join( import.meta.dirname!, "shims", "supports-color", "index.ts", ), }; export function shims(): Plugin { return { name: "fresh:shims", sharedDuringBuild: true, resolveId: { filter: { id: /(object\.entries|supports-color)/, }, handler(id) { const resolved = SHIMS[id]; if (resolved !== undefined) { return resolved; } }, }, }; } ================================================ FILE: packages/plugin-vite/src/plugins/verify_imports.ts ================================================ import type { Plugin } from "vite"; import * as cl from "@std/fmt/colors"; import type { PluginContext } from "rollup"; import path from "node:path"; import { pathWithRoot } from "../utils.ts"; /** A diagnostic message for an invalid import */ export interface ImportCheckDiagnostic { type: "warn" | "error"; message: string; description?: string; hint?: string; } /** A check whether or not an import is valid or not for an environment */ export type ImportCheck = ( id: string, env: string, ) => ImportCheckDiagnostic | void; export interface CheckImportOptions { checks: ImportCheck[]; } export function checkImports(pluginOptions: CheckImportOptions): Plugin { function check( options: CheckImportOptions, id: string, env: "server" | "client", ): ImportCheckDiagnostic | undefined { for (let i = 0; i < options.checks.length; i++) { const check = options.checks[i]; const result = check(id, env); if (result) return result; } } let root = ""; let isDev = false; const seen = new Set(); return { name: "fresh:check-imports", sharedDuringBuild: true, enforce: "pre", applyToEnvironment() { return true; }, config(_, env) { isDev = env.command === "serve"; }, configResolved(config) { root = pathWithRoot(config.root); }, resolveId: { filter: { id: [ /^(?!\0|[\\/]@fs[\\/]|fresh-island::|fresh:)/, /[\\/]node_modules[\\/]/, ], }, async handler(id, importer) { if ( importer && (importer.startsWith("\0") || importer.includes("node_modules") || importer.includes("deno::")) ) { return; } let result: ImportCheckDiagnostic | undefined; if (id.startsWith(".")) { const resolved = await this.resolve(id, importer); if (resolved !== null) { const key = `${this.environment.config.consumer}::${resolved.id}::${importer}`; if (!seen.has(key)) { result = check( pluginOptions, resolved.id, this.environment.config.consumer, ); } seen.add(key); } } else { const key = `${this.environment.config.consumer}::${id}::${importer}`; if (!seen.has(key)) { result = check(pluginOptions, id, this.environment.config.consumer); } seen.add(key); } if (result) { const label = result.type === "warn" ? cl.inverse(cl.yellow(` WARN `)) : cl.inverse(cl.red(` ERROR `)); // deno-lint-ignore no-console console.log(); // deno-lint-ignore no-console console.log(); // deno-lint-ignore no-console console.log(`${label} ${result.message}`); // deno-lint-ignore no-console console.log(); if (importer !== undefined) { const ancestors = findAncestors(this, importer, isDev); if (ancestors && ancestors.length > 0) { // deno-lint-ignore no-console console.log( `The specifier ${cl.cyan(`"${id}"`)} was imported in:`, ); ancestors.forEach((spec) => { if (path.isAbsolute(spec)) { spec = path.relative(root, spec); } // deno-lint-ignore no-console console.log(` - ${cl.cyan(spec)}`); }); // deno-lint-ignore no-console console.log(); } } if (result.hint) { // deno-lint-ignore no-console console.log(cl.bold(` hint: `) + result.hint); // deno-lint-ignore no-console console.log(); } if (result.type === "error") { this.error({ message: result.message, id: importer, }); } else { this.warn({ message: result.message, id: importer, }); } } }, }, }; } function findAncestors( ctx: PluginContext, id: string, isDev: boolean, ): string[] | null { const mod = ctx.getModuleInfo(id); if (mod === null) return null; if (isDev || mod.importers.length === 0) { return [id]; } for (let i = 0; i < mod.importers.length; i++) { const importer = mod.importers[i]; const result = findAncestors(ctx, importer, isDev); if (result !== null) { result.push(id); return result; } } return null; } ================================================ FILE: packages/plugin-vite/src/shared.ts ================================================ export function hashCode(moduleId: string) { let hash = 0, i, chr; if (moduleId.length === 0) return hash; for (i = 0; i < moduleId.length; i++) { chr = moduleId.charCodeAt(i); hash = (hash << 5) - hash + chr; hash |= 0; // Convert to 32bit integer } return hash; } ================================================ FILE: packages/plugin-vite/src/utils.ts ================================================ import * as path from "@std/path"; import type { FsRouteFileNoMod, UniqueNamer } from "fresh/internal-dev"; import type { ImportCheck } from "./plugins/verify_imports.ts"; export const JS_REG = /\.([tj]sx?|[mc]?[tj]s)(\?.*)?$/; export const JSX_REG = /\.[tj]sx(\?.*)?$/; export function pathWithRoot(fileOrDir: string, root?: string): string { if (path.isAbsolute(fileOrDir)) return fileOrDir; if (root === undefined) { return path.join(Deno.cwd(), fileOrDir); } if (path.isAbsolute(root)) return path.join(root, fileOrDir); return path.join(Deno.cwd(), root, fileOrDir); } export interface FreshState { namer: UniqueNamer; root: string; serverEntry: string; islandDir: string; routeDir: string; dev: boolean; islands: Map; // deno-lint-ignore no-explicit-any routes: FsRouteFileNoMod[]; clientOutDir: string; serverOutDir: string; } export interface ClientSnapshot { entry: string; } /** Configuration options for Fresh when using the Vite plugin */ export interface FreshViteConfig { /** Path to main server entry file. Default: `main.ts` */ serverEntry?: string; /** Path to main client entry file. Default: `client.ts` */ clientEntry?: string; /** Path to islands directory. Default: `./islands` */ islandsDir?: string; /** Path to routes directory. Default: `./routes` */ routeDir?: string; /** * Ignore file paths matching any of the provided regexes when * crawling the islands and routes directories. */ ignore?: RegExp[]; /** * Treat these specifiers as island files. This is used to declare * islands from remote packages. */ islandSpecifiers?: string[]; /** * A list of checks that will be performed for imports. * * Can be used to warn or error when certain imports exist in * server or client code. Useful to enforce that some dependencies * are not imported in Islands running in the browser. */ checkImports?: ImportCheck[]; } export type ResolvedFreshViteConfig = & Required< Omit > & { islandSpecifiers: Map; namer: UniqueNamer }; ================================================ FILE: packages/plugin-vite/tests/build_test.ts ================================================ import { expect } from "@std/expect"; import { waitFor, waitForText, withBrowser, } from "../../fresh/tests/test_utils.tsx"; import { buildVite, DEMO_DIR, FIXTURE_DIR, launchProd, usingEnv, } from "./test_utils.ts"; import * as path from "@std/path"; const viteResult = await buildVite(DEMO_DIR); Deno.test({ name: "vite build - launches", fn: async () => { await launchProd( { cwd: viteResult.tmp }, async (address) => { const res = await fetch(address); const text = await res.text(); expect(text).toEqual("it works"); }, ); }, sanitizeOps: false, sanitizeResources: false, }); Deno.test({ name: "vite build - creates compiled entry", fn: async () => { const stat = await Deno.stat( path.join(viteResult.tmp, "_fresh", "compiled-entry.js"), ); expect(stat.isFile).toEqual(true); }, sanitizeOps: false, sanitizeResources: false, }); Deno.test({ name: "vite build - serves static files", fn: async () => { await launchProd( { cwd: viteResult.tmp }, async (address) => { const res = await fetch(`${address}/test_static/foo.txt`); const text = await res.text(); expect(text).toEqual("it works"); }, ); }, sanitizeOps: false, sanitizeResources: false, }); Deno.test({ name: "vite build - loads islands", fn: async () => { await launchProd( { cwd: viteResult.tmp }, async (address) => { await withBrowser(async (page) => { await page.goto(`${address}/tests/island_hooks`, { waitUntil: "networkidle2", }); await waitForText(page, "button", "count: 0"); await page.locator("button").click(); await waitForText(page, "button", "count: 1"); }); }, ); }, sanitizeOps: false, sanitizeResources: false, }); Deno.test({ name: "vite build - nested islands", fn: async () => { await launchProd( { cwd: viteResult.tmp }, async (address) => { await withBrowser(async (page) => { await page.goto(`${address}/tests/island_nested`, { waitUntil: "networkidle2", }); await page.locator(".outer-ready").wait(); await page.locator(".inner-ready").wait(); }); }, ); }, sanitizeOps: false, sanitizeResources: false, }); Deno.test({ name: "vite build - without static/ dir", fn: async () => { const fixture = path.join(FIXTURE_DIR, "no_static"); await using res = await buildVite(fixture); await launchProd( { cwd: res.tmp }, async (address) => { const res = await fetch(`${address}/ok`); const text = await res.text(); expect(text).toEqual("ok"); }, ); }, sanitizeOps: false, sanitizeResources: false, }); Deno.test({ name: "vite build - without islands/ dir", fn: async () => { const fixture = path.join(FIXTURE_DIR, "no_islands"); await using res = await buildVite(fixture); await launchProd( { cwd: res.tmp }, async (address) => { const res = await fetch(`${address}`); const text = await res.text(); expect(text).toContain("ok"); }, ); }, sanitizeOps: false, sanitizeResources: false, }); Deno.test({ name: "vite build - without routes/ dir", fn: async () => { const fixture = path.join(FIXTURE_DIR, "no_routes"); await using res = await buildVite(fixture); await launchProd( { cwd: res.tmp }, async (address) => { const res = await fetch(`${address}`); const text = await res.text(); expect(text).toEqual("ok"); }, ); }, sanitizeOps: false, sanitizeResources: false, }); Deno.test({ name: "vite build - load json inside npm package", fn: async () => { await launchProd( { cwd: viteResult.tmp }, async (address) => { await withBrowser(async (page) => { await page.goto(`${address}/tests/mime`, { waitUntil: "networkidle2", }); await page.locator(".ready").wait(); }); }, ); }, sanitizeOps: false, sanitizeResources: false, }); Deno.test({ name: "vite build - fetch static assets", fn: async () => { await launchProd( { cwd: viteResult.tmp }, async (address) => { await withBrowser(async (page) => { await page.goto(`${address}/tests/assets`, { waitUntil: "networkidle2", }); const url = await page.locator("img").evaluate((el) => // deno-lint-ignore no-explicit-any (el as any).src ); const res = await fetch(url); await res.body?.cancel(); expect(res.status).toEqual(200); expect(res.headers.get("Content-Type")).toEqual("image/png"); }); }, ); }, sanitizeOps: false, sanitizeResources: false, }); Deno.test({ name: "vite build - tailwind no _app", fn: async () => { const fixture = path.join(FIXTURE_DIR, "tailwind_no_app"); await using res = await buildVite(fixture); await launchProd( { cwd: res.tmp }, async (address) => { await withBrowser(async (page) => { await page.goto(`${address}`, { waitUntil: "networkidle2", }); const href = await page .locator("link[rel='stylesheet']") .evaluate((el) => { // deno-lint-ignore no-explicit-any return (el as any).href; }); expect(href).toMatch(/\/assets\/client-entry-.*\.css(\?.*)?$/); }); }, ); }, sanitizeOps: false, sanitizeResources: false, }); Deno.test({ name: "vite build - tailwind _app", fn: async () => { const fixture = path.join(FIXTURE_DIR, "tailwind_app"); await using res = await buildVite(fixture); await launchProd( { cwd: res.tmp }, async (address) => { await withBrowser(async (page) => { await page.goto(`${address}`, { waitUntil: "networkidle2", }); const href = await page .locator("link[rel='stylesheet']") .evaluate((el) => { // deno-lint-ignore no-explicit-any return (el as any).href; }); expect(href).toMatch(/\/assets\/client-entry-.*\.css/); }); }, ); }, sanitizeOps: false, sanitizeResources: false, }); Deno.test({ name: "vite build - partial island", fn: async () => { await launchProd( { cwd: viteResult.tmp }, async (address) => { await withBrowser(async (page) => { await page.goto(`${address}/tests/partial`, { waitUntil: "networkidle2", }); await page.locator(".ready").wait(); await page.locator("a").click(); await page.locator(".counter-hooks").wait(); await page.locator(".counter-hooks button").click(); await waitForText(page, ".counter-hooks button", "count: 1"); }); }, ); }, sanitizeOps: false, sanitizeResources: false, }); Deno.test({ name: "vite build - build ID uses env variables when set", fn: async () => { const revision = "test-commit-hash-123"; // We're running on GitHub Actions, so GITHUB_SHA will always // be set Deno.env.delete("GITHUB_SHA"); for ( const key of [ "DENO_DEPLOYMENT_ID", "GITHUB_SHA", "CI_COMMIT_SHA", "OTHER", ] ) { using _ = usingEnv(key, revision); await using res = await buildVite(DEMO_DIR); await launchProd( { cwd: res.tmp }, async (address) => { const res = await fetch(`${address}/tests/build_id`); const text = await res.text(); if (key === "OTHER") { expect(text).not.toEqual(revision); } else { expect(text).toEqual(revision); } }, ); } }, sanitizeOps: false, sanitizeResources: false, }); Deno.test({ name: "vite build - import json from jsr dependency", fn: async () => { await launchProd( { cwd: viteResult.tmp }, async (address) => { const res = await fetch(`${address}/tests/dep_json`); const json = await res.json(); expect(json.name).toEqual("@marvinh-test/import-json"); }, ); }, sanitizeOps: false, sanitizeResources: false, }); Deno.test({ name: "vite build - import node:*", fn: async () => { await launchProd( { cwd: viteResult.tmp }, async (address) => { const res = await fetch(`${address}/tests/feed`); await res.body?.cancel(); expect(res.status).toEqual(200); }, ); }, sanitizeOps: false, sanitizeResources: false, }); Deno.test({ name: "vite build - css modules", fn: async () => { await launchProd( { cwd: viteResult.tmp }, async (address) => { await withBrowser(async (page) => { await page.goto(`${address}/tests/css_modules`, { waitUntil: "networkidle2", }); let color = await page .locator(".red > h1") // deno-lint-ignore no-explicit-any .evaluate((el) => window.getComputedStyle(el as any).color); expect(color).toEqual("rgb(255, 0, 0)"); color = await page .locator(".green > h1") // deno-lint-ignore no-explicit-any .evaluate((el) => window.getComputedStyle(el as any).color); expect(color).toEqual("rgb(0, 128, 0)"); color = await page .locator(".blue > h1") // deno-lint-ignore no-explicit-any .evaluate((el) => window.getComputedStyle(el as any).color); expect(color).toEqual("rgb(0, 0, 255)"); // Route css color = await page .locator(".route > h1") // deno-lint-ignore no-explicit-any .evaluate((el) => window.getComputedStyle(el as any).color); expect(color).toEqual("rgb(255, 218, 185)"); }); }, ); }, sanitizeOps: false, sanitizeResources: false, }); Deno.test({ name: "vite build - route css import", fn: async () => { await launchProd( { cwd: viteResult.tmp }, async (address) => { await withBrowser(async (page) => { await page.goto(`${address}/tests/css`, { waitUntil: "networkidle2", }); await waitFor(async () => { const color = await page .locator("h1") // deno-lint-ignore no-explicit-any .evaluate((el) => window.getComputedStyle(el as any).color); expect(color).toEqual("rgb(255, 0, 0)"); return true; }); }); }, ); }, sanitizeOps: false, sanitizeResources: false, }); Deno.test({ name: "vite build - remote island", fn: async () => { const fixture = path.join(FIXTURE_DIR, "remote_island"); await using res = await buildVite(fixture); await launchProd( { cwd: res.tmp }, async (address) => { await withBrowser(async (page) => { await page.goto(`${address}`, { waitUntil: "networkidle2", }); await page.locator(".remote-island").wait(); await page.locator(".increment").click(); await waitForText(page, ".result", "Count: 1"); }); }, ); }, sanitizeOps: false, sanitizeResources: false, }); Deno.test({ name: "vite build - error on 'node:process' import", fn: async () => { const fixture = path.join(FIXTURE_DIR, "node_builtin"); await expect(buildVite(fixture)).rejects.toThrow( "Node built-in modules cannot be imported in the browser", ); }, sanitizeOps: false, sanitizeResources: false, }); Deno.test({ name: "vite build - static index.html", fn: async () => { await launchProd( { cwd: viteResult.tmp }, async (address) => { const res = await fetch(`${address}/test_static/foo`); const text = await res.text(); expect(text).toContain("

ok

"); }, ); }, sanitizeOps: false, sanitizeResources: false, }); Deno.test({ name: "vite build - base path asset handling", fn: async () => { await using res = await buildVite(DEMO_DIR, { base: "/my-app/" }); // Read the generated server.js to check asset paths const serverJs = await Deno.readTextFile( path.join(res.tmp, "_fresh", "server.js"), ); // Asset paths should include the base path /my-app/ expect(serverJs).toContain('"/my-app/assets/'); }, sanitizeOps: false, sanitizeResources: false, }); Deno.test({ name: "vite build - env files", fn: async () => { await launchProd( { cwd: viteResult.tmp }, async (address) => { const res = await fetch(`${address}/tests/env_files`); const json = await res.json(); expect(json).toEqual({ MY_ENV: "MY_ENV test value", VITE_MY_ENV: "VITE_MY_ENV test value", MY_LOCAL_ENV: "MY_LOCAL_ENV test value", VITE_MY_LOCAL_ENV: "VITE_MY_LOCAL_ENV test value", }); }, ); }, sanitizeOps: false, sanitizeResources: false, }); Deno.test({ name: "vite build - support _middleware Array", fn: async () => { await launchProd( { cwd: viteResult.tmp }, async (address) => { const res = await fetch(`${address}/tests/middlewares`); const text = await res.text(); expect(text).toEqual("AB"); }, ); }, sanitizeOps: false, sanitizeResources: false, }); Deno.test({ name: "vite build - island named after global object (Map)", fn: async () => { const fixture = path.join(FIXTURE_DIR, "island_global_name"); await using res = await buildVite(fixture); await launchProd( { cwd: res.tmp }, async (address) => { const response = await fetch(address); const html = await response.text(); // Verify the fix: UniqueNamer prefixes "Map" with underscore to prevent shadowing // Without the fix: import Map from "..." and boot({Map}, ...) - shadows global Map // With the fix: import _Map_N from "..." and boot({_Map_N}, ...) - no shadowing expect(html).toMatch(/import.*_Map(_\d+)?.*from/); expect(html).toMatch(/boot\(\s*\{\s*_Map(_\d+)?\s*\}/); }, ); }, sanitizeOps: false, sanitizeResources: false, }); Deno.test({ name: "vite build - excludes test files from routes", fn: async () => { const fixture = path.join(FIXTURE_DIR, "test_files_exclusion"); await using res = await buildVite(fixture); // Verify that test files in routes/ are not bundled const serverAssetsDir = path.join(res.tmp, "_fresh", "server", "assets"); // List all compiled route files const files: string[] = []; for await (const entry of Deno.readDir(serverAssetsDir)) { if (entry.isFile && entry.name.startsWith("_fresh-route")) { files.push(entry.name); } } // Should have index route but NOT test files expect(files.some((f) => f.includes("_fresh-route___index"))).toBe(true); expect(files.some((f) => f.includes("index_test"))).toBe(false); expect(files.some((f) => f.includes("foo.test"))).toBe(false); // Verify no test pattern files at all // Note: files with "_test" or ".test" in their name (not just a "tests" folder) const testPatterns = ["_test.", "_test-", ".test."]; for (const file of files) { for (const pattern of testPatterns) { expect(file.includes(pattern)).toBe(false); } } }, sanitizeOps: false, sanitizeResources: false, }); ================================================ FILE: packages/plugin-vite/tests/dev_server_test.ts ================================================ import * as path from "@std/path"; import { expect } from "@std/expect"; import { waitFor, waitForText, withBrowser, } from "../../fresh/tests/test_utils.tsx"; import { DEMO_DIR, FIXTURE_DIR, launchDevServer, prepareDevServer, spawnDevServer, updateFile, withDevServer, } from "./test_utils.ts"; const tmp = await prepareDevServer(DEMO_DIR); const demoServer = await spawnDevServer(tmp.dir, { FRESH_PUBLIC_FOO: "foobar", }); Deno.test({ name: "vite dev - launches", fn: async () => { const res = await fetch(`${demoServer.address()}/tests/it_works`); const text = await res.text(); expect(text).toContain("it works"); }, sanitizeResources: false, sanitizeOps: false, }); Deno.test({ name: "vite dev - serves static files", fn: async () => { const res = await fetch(`${demoServer.address()}/test_static/foo.txt`); const text = await res.text(); expect(text).toContain("it works"); }, sanitizeResources: false, sanitizeOps: false, }); Deno.test({ name: "vite dev - loads islands", fn: async () => { await withBrowser(async (page) => { await page.goto(`${demoServer.address()}/tests/island_hooks`, { waitUntil: "networkidle2", }); await waitForText(page, "button", "count: 0"); await page.locator("button").click(); await waitForText(page, "button", "count: 1"); }); }, sanitizeResources: false, sanitizeOps: false, }); Deno.test({ name: "vite dev - starts without static/ dir", fn: async () => { const fixture = path.join(FIXTURE_DIR, "no_static"); await withDevServer(fixture, async (address) => { const res = await fetch(`${address}/`); const text = await res.text(); expect(text).toContain("ok"); }); }, sanitizeResources: false, sanitizeOps: false, }); Deno.test({ name: "vite dev - starts without islands/ dir", fn: async () => { const fixture = path.join(FIXTURE_DIR, "no_islands"); await withDevServer(fixture, async (address) => { const res = await fetch(`${address}/`); const text = await res.text(); expect(text).toContain("ok"); }); }, sanitizeResources: false, sanitizeOps: false, }); Deno.test({ name: "vite dev - starts without routes/ dir", fn: async () => { const fixture = path.join(FIXTURE_DIR, "no_routes"); await withDevServer(fixture, async (address) => { const res = await fetch(`${address}/`); const text = await res.text(); expect(text).toContain("ok"); }); }, sanitizeResources: false, sanitizeOps: false, }); Deno.test({ name: "vite dev - can apply HMR to islands (hooks)", ignore: true, // Test is very flaky fn: async () => { await withBrowser(async (page) => { await page.goto(`${demoServer.address()}/tests/island_hooks`, { waitUntil: "networkidle2", }); await waitForText(page, "button", "count: 0"); await page.locator("button").click(); await waitForText(page, "button", "count: 1"); const island = path.join( demoServer.dir, "islands", "tests", "CounterHooks.tsx", ); await using _ = await updateFile( island, (text) => text.replace("count:", "hmr:"), ); await waitForText(page, "button", "hmr: 1"); await page.locator("button").click(); await waitForText(page, "button", "hmr: 2"); }); }, sanitizeResources: false, sanitizeOps: false, }); Deno.test({ name: "vite dev - can import json in npm package", fn: async () => { await withBrowser(async (page) => { await page.goto(`${demoServer.address()}/tests/mime`, { waitUntil: "networkidle2", }); await page.locator(".ready").wait(); }); }, sanitizeResources: false, sanitizeOps: false, }); Deno.test({ name: "vite dev - inline env vars", fn: async () => { await withBrowser(async (page) => { await page.goto(`${demoServer.address()}/tests/env`, { waitUntil: "networkidle2", }); await page.locator(".ready").wait(); const res = await page.locator("pre").evaluate((el) => // deno-lint-ignore no-explicit-any (el as any).textContent ?? "" ); expect(JSON.parse(res)).toEqual({ deno: "foobar", nodeEnv: "foobar" }); }); }, sanitizeResources: false, sanitizeOps: false, }); Deno.test({ name: "vite dev - serves imported assets", fn: async () => { // Vite has an internal allowlist that is refreshed when // pathnames are requested. It will discover valid static // files imported in JS once it encounters them. Therefore // we must request the URL that ultimately imports the // asset first for it to work. let res = await fetch(`${demoServer.address()}/tests/assets`); await res.body?.cancel(); res = await fetch(`${demoServer.address()}/assets/deno-logo.png`); expect(res.status).toEqual(200); expect(res.headers.get("Content-Type")).toEqual("image/png"); }, sanitizeResources: false, sanitizeOps: false, }); Deno.test({ name: "vite dev - tailwind no _app", fn: async () => { const fixture = path.join(FIXTURE_DIR, "tailwind_no_app"); await withDevServer(fixture, async (address) => { await withBrowser(async (page) => { await page.goto(`${address}`, { waitUntil: "networkidle2", }); await page.locator("style[data-vite-dev-id$='style.css']").wait(); }); }); }, sanitizeResources: false, sanitizeOps: false, }); Deno.test({ name: "vite dev - tailwind _app", fn: async () => { const fixture = path.join(FIXTURE_DIR, "tailwind_app"); await withDevServer(fixture, async (address) => { await withBrowser(async (page) => { await page.goto(`${address}`, { waitUntil: "networkidle2", }); await page.locator("style[data-vite-dev-id$='style.css']").wait(); }); }); }, sanitizeResources: false, sanitizeOps: false, }); Deno.test({ name: "vite dev - partial island", fn: async () => { await withBrowser(async (page) => { await page.goto(`${demoServer.address()}/tests/partial`, { waitUntil: "networkidle2", }); await page.locator(".ready").wait(); await page.locator("a").click(); await page.locator(".counter-hooks").wait(); await page.locator(".counter-hooks button").click(); await waitForText(page, ".counter-hooks button", "count: 1"); }); }, sanitizeResources: false, sanitizeOps: false, }); Deno.test({ name: "vite dev - json from jsr dependency", fn: async () => { const res = await fetch(`${demoServer.address()}/tests/dep_json`); const json = await res.json(); expect(json.name).toEqual("@marvinh-test/import-json"); }, sanitizeResources: false, sanitizeOps: false, }); Deno.test({ name: "vite dev - import node:*", fn: async () => { const res = await fetch(`${demoServer.address()}/tests/feed`); await res.body?.cancel(); expect(res.status).toEqual(200); }, sanitizeResources: false, sanitizeOps: false, }); Deno.test({ name: "vite dev - css modules", fn: async () => { await withBrowser(async (page) => { await page.goto(`${demoServer.address()}/tests/css_modules`, { waitUntil: "networkidle2", }); await waitFor(async () => { let color = await page .locator(".red > h1") // deno-lint-ignore no-explicit-any .evaluate((el) => window.getComputedStyle(el as any).color); expect(color).toEqual("rgb(255, 0, 0)"); color = await page .locator(".green > h1") // deno-lint-ignore no-explicit-any .evaluate((el) => window.getComputedStyle(el as any).color); expect(color).toEqual("rgb(0, 128, 0)"); color = await page .locator(".blue > h1") // deno-lint-ignore no-explicit-any .evaluate((el) => window.getComputedStyle(el as any).color); expect(color).toEqual("rgb(0, 0, 255)"); // Route css color = await page .locator(".route > h1") // deno-lint-ignore no-explicit-any .evaluate((el) => window.getComputedStyle(el as any).color); expect(color).toEqual("rgb(255, 218, 185)"); return true; }); }); }, sanitizeResources: false, sanitizeOps: false, }); Deno.test({ name: "vite dev - route css import", fn: async () => { await withBrowser(async (page) => { await page.goto(`${demoServer.address()}/tests/css`, { waitUntil: "networkidle2", }); await waitFor(async () => { const color = await page .locator("h1") // deno-lint-ignore no-explicit-any .evaluate((el) => window.getComputedStyle(el as any).color); expect(color).toEqual("rgb(255, 0, 0)"); return true; }); }); }, sanitizeResources: false, sanitizeOps: false, }); Deno.test({ name: "vite dev - nested islands", fn: async () => { await withBrowser(async (page) => { await page.goto(`${demoServer.address()}/tests/island_nested`, { waitUntil: "networkidle2", }); await page.locator(".outer-ready").wait(); await page.locator(".inner-ready").wait(); }); }, sanitizeResources: false, sanitizeOps: false, }); Deno.test({ name: "vite dev - remote island", fn: async () => { const fixture = path.join(FIXTURE_DIR, "remote_island"); await launchDevServer(fixture, async (address) => { await withBrowser(async (page) => { await page.goto(`${address}`, { waitUntil: "networkidle2", }); await page.locator(".remote-island").wait(); await page.locator(".increment").click(); await waitForText(page, ".result", "Count: 1"); }); }); }, sanitizeResources: false, sanitizeOps: false, }); Deno.test({ name: "vite dev - error on 'node:process' import", fn: async () => { const fixture = path.join(FIXTURE_DIR, "node_builtin"); await launchDevServer(fixture, async (address) => { let res = await fetch(`${address}`); await res.body?.cancel(); res = await fetch(`${address}/@id/fresh-island::NodeIsland`); await res.body?.cancel(); expect(res.status).toEqual(500); }); }, sanitizeOps: false, sanitizeResources: false, }); // issue: https://github.com/denoland/fresh/issues/3322 Deno.test({ name: "vite dev - allow routes looking like static paths", fn: async () => { const res = await fetch( `${demoServer.address()}/tests/api/@marvinh@infosec.exchange`, ); const text = await res.text(); expect(text).toEqual("ok"); }, sanitizeOps: false, sanitizeResources: false, }); // issue: https://github.com/denoland/fresh/issues/3323 Deno.test({ name: "vite dev - npm:pg", fn: async () => { const res = await fetch(`${demoServer.address()}/tests/pg`); const text = await res.text(); expect(text).toContain("

pg

"); }, sanitizeOps: false, sanitizeResources: false, }); Deno.test({ name: "vite dev - npm:ioredis", fn: async () => { const res = await fetch(`${demoServer.address()}/tests/ioredis`); const text = await res.text(); expect(text).toContain("

ioredis

"); }, sanitizeOps: false, sanitizeResources: false, }); Deno.test({ name: "vite dev - redis", fn: async () => { const res = await fetch(`${demoServer.address()}/tests/redis`); const text = await res.text(); expect(text).toContain("

redis

"); }, sanitizeOps: false, sanitizeResources: false, }); Deno.test({ name: "vite dev - @supabase/postgres-js", fn: async () => { const res = await fetch(`${demoServer.address()}/tests/supabase_pg`); const text = await res.text(); expect(text).toContain("

supabase

"); }, sanitizeOps: false, sanitizeResources: false, }); Deno.test({ name: "vite dev - radix", fn: async () => { const res = await fetch(`${demoServer.address()}/tests/radix`); const text = await res.text(); expect(text).toContain("click me"); }, sanitizeOps: false, sanitizeResources: false, }); Deno.test({ name: "vite dev - qs", fn: async () => { const res = await fetch(`${demoServer.address()}/tests/qs`); const text = await res.text(); expect(text).toContain("

qs

"); }, sanitizeOps: false, sanitizeResources: false, }); Deno.test({ name: "vite dev - stripe", fn: async () => { const res = await fetch(`${demoServer.address()}/tests/stripe`); const text = await res.text(); expect(text).toContain("

stripe

"); }, sanitizeOps: false, sanitizeResources: false, }); Deno.test({ name: "vite dev - static index.html", fn: async () => { const res = await fetch(`${demoServer.address()}/test_static/foo`); const text = await res.text(); expect(text).toContain("

ok

"); }, sanitizeOps: false, sanitizeResources: false, }); Deno.test({ name: "vite dev - load .env files", fn: async () => { const res = await fetch(`${demoServer.address()}/tests/env_files`); const json = await res.json(); expect(json).toEqual({ MY_ENV: "MY_ENV test value", VITE_MY_ENV: "VITE_MY_ENV test value", MY_LOCAL_ENV: "MY_LOCAL_ENV test value", VITE_MY_LOCAL_ENV: "VITE_MY_LOCAL_ENV test value", }); }, sanitizeOps: false, sanitizeResources: false, }); Deno.test({ name: "vite dev - support _middleware Array", fn: async () => { const res = await fetch(`${demoServer.address()}/tests/middlewares`); const text = await res.text(); expect(text).toEqual("AB"); }, sanitizeOps: false, sanitizeResources: false, }); Deno.test({ name: "vite dev - support jsx namespace", fn: async () => { const res = await fetch(`${demoServer.address()}/tests/jsx_namespace`); const text = await res.text(); expect(text).toContain(`xml:space="preserve"`); }, sanitizeOps: false, sanitizeResources: false, }); Deno.test({ name: "vite dev - source mapped stack traces", fn: async () => { const res = await fetch(`${demoServer.address()}/tests/throw`); const text = await res.text(); expect(text).toContain("throw.tsx:5:11"); }, sanitizeOps: false, sanitizeResources: false, }); ================================================ FILE: packages/plugin-vite/tests/fixtures/deno_global_island/islands/Foo.tsx ================================================ export function Foo() { return

{Deno.cwd()}

; } ================================================ FILE: packages/plugin-vite/tests/fixtures/deno_global_island/main.ts ================================================ import { App, staticFiles } from "@fresh/core"; export const app = new App() .use(staticFiles()) .fsRoutes(); ================================================ FILE: packages/plugin-vite/tests/fixtures/deno_global_island/routes/index.tsx ================================================ import { Foo } from "../islands/Foo.tsx"; export default function Hello() { return ; } ================================================ FILE: packages/plugin-vite/tests/fixtures/deno_global_island/vite.config.ts ================================================ import { defineConfig } from "vite"; import { fresh } from "@fresh/plugin-vite"; export default defineConfig({ plugins: [fresh()], }); ================================================ FILE: packages/plugin-vite/tests/fixtures/deno_global_ssr/main.ts ================================================ import { App, staticFiles } from "@fresh/core"; export const app = new App() .use(staticFiles()) .fsRoutes(); ================================================ FILE: packages/plugin-vite/tests/fixtures/deno_global_ssr/routes/index.tsx ================================================ export default function Hello() { return

{Deno.cwd()}

; } ================================================ FILE: packages/plugin-vite/tests/fixtures/deno_global_ssr/vite.config.ts ================================================ import { defineConfig } from "vite"; import { fresh } from "@fresh/plugin-vite"; export default defineConfig({ plugins: [fresh()], }); ================================================ FILE: packages/plugin-vite/tests/fixtures/island_global_name/deno.json ================================================ { "imports": { "fresh": "jsr:@fresh/core", "@preact/signals": "npm:@preact/signals@^1.3.0", "preact": "npm:preact@^10.26.1", "preact/": "npm:/preact@^10.26.1/" } } ================================================ FILE: packages/plugin-vite/tests/fixtures/island_global_name/islands/Map.tsx ================================================ import { useSignal } from "@preact/signals"; import { useEffect } from "preact/hooks"; export default function Map() { const ready = useSignal(false); const count = useSignal(0); useEffect(() => { ready.value = true; }, []); return (

{count.value}

); } ================================================ FILE: packages/plugin-vite/tests/fixtures/island_global_name/main.ts ================================================ import { App } from "fresh"; export const app = new App().fsRoutes(); ================================================ FILE: packages/plugin-vite/tests/fixtures/island_global_name/routes/index.tsx ================================================ import Map from "../islands/Map.tsx"; export default function Page() { return ; } ================================================ FILE: packages/plugin-vite/tests/fixtures/island_global_name/vite.config.ts ================================================ import { defineConfig } from "vite"; import { fresh } from "@fresh/plugin-vite"; export default defineConfig({ plugins: [fresh()], }); ================================================ FILE: packages/plugin-vite/tests/fixtures/no_islands/main.ts ================================================ import { App } from "@fresh/core"; export const app = new App().fsRoutes(); ================================================ FILE: packages/plugin-vite/tests/fixtures/no_islands/routes/index.tsx ================================================ export default function Hello() { return

ok

; } ================================================ FILE: packages/plugin-vite/tests/fixtures/no_islands/vite.config.ts ================================================ import { defineConfig } from "vite"; import { fresh } from "@fresh/plugin-vite"; export default defineConfig({ plugins: [fresh()], }); ================================================ FILE: packages/plugin-vite/tests/fixtures/no_routes/main.ts ================================================ import { App } from "@fresh/core"; export const app = new App().get("/", () => new Response("ok")); ================================================ FILE: packages/plugin-vite/tests/fixtures/no_routes/vite.config.ts ================================================ import { defineConfig } from "vite"; import { fresh } from "@fresh/plugin-vite"; export default defineConfig({ plugins: [fresh()], }); ================================================ FILE: packages/plugin-vite/tests/fixtures/no_static/main.ts ================================================ import { App } from "@fresh/core"; export const app = new App().get("/ok", () => new Response("ok")).fsRoutes(); ================================================ FILE: packages/plugin-vite/tests/fixtures/no_static/routes/index.tsx ================================================ export default function Hello() { return

ok

; } ================================================ FILE: packages/plugin-vite/tests/fixtures/no_static/vite.config.ts ================================================ import { defineConfig } from "vite"; import { fresh } from "@fresh/plugin-vite"; export default defineConfig({ plugins: [fresh()], }); ================================================ FILE: packages/plugin-vite/tests/fixtures/node_builtin/islands/NodeIsland.tsx ================================================ import process from "node:process"; export function NodeIsland() { return

{process.version}

; } ================================================ FILE: packages/plugin-vite/tests/fixtures/node_builtin/main.ts ================================================ import { App, staticFiles } from "@fresh/core"; export const app = new App() .use(staticFiles()) .fsRoutes(); ================================================ FILE: packages/plugin-vite/tests/fixtures/node_builtin/routes/index.tsx ================================================ import { NodeIsland } from "../islands/NodeIsland.tsx"; export default function Hello() { return ; } ================================================ FILE: packages/plugin-vite/tests/fixtures/node_builtin/vite.config.ts ================================================ import { defineConfig } from "vite"; import { fresh } from "@fresh/plugin-vite"; export default defineConfig({ plugins: [fresh()], }); ================================================ FILE: packages/plugin-vite/tests/fixtures/remote_island/main.ts ================================================ import { App, staticFiles } from "@fresh/core"; export const app = new App() .use(staticFiles()) .fsRoutes(); ================================================ FILE: packages/plugin-vite/tests/fixtures/remote_island/routes/index.tsx ================================================ import { RemoteIsland } from "@marvinh-test/fresh-island"; export default function Hello() { return ; } ================================================ FILE: packages/plugin-vite/tests/fixtures/remote_island/vite.config.ts ================================================ import { defineConfig } from "vite"; import { fresh } from "@fresh/plugin-vite"; export default defineConfig({ plugins: [fresh({ islandSpecifiers: ["@marvinh-test/fresh-island"], })], }); ================================================ FILE: packages/plugin-vite/tests/fixtures/tailwind_app/assets/style.css ================================================ @import "tailwindcss"; ================================================ FILE: packages/plugin-vite/tests/fixtures/tailwind_app/client.ts ================================================ import "./assets/style.css"; ================================================ FILE: packages/plugin-vite/tests/fixtures/tailwind_app/main.ts ================================================ import { App } from "@fresh/core"; export const app = new App().fsRoutes(); ================================================ FILE: packages/plugin-vite/tests/fixtures/tailwind_app/routes/_app.tsx ================================================ import type { PageProps } from "@fresh/core"; export default function App(props: PageProps) { return ( ); } ================================================ FILE: packages/plugin-vite/tests/fixtures/tailwind_app/routes/index.tsx ================================================ export default function Hello() { return

ok

; } ================================================ FILE: packages/plugin-vite/tests/fixtures/tailwind_app/vite.config.ts ================================================ import { defineConfig } from "vite"; import { fresh } from "@fresh/plugin-vite"; import tailwindcss from "@tailwindcss/vite"; export default defineConfig({ plugins: [fresh(), tailwindcss()], }); ================================================ FILE: packages/plugin-vite/tests/fixtures/tailwind_no_app/assets/style.css ================================================ @import "tailwindcss"; ================================================ FILE: packages/plugin-vite/tests/fixtures/tailwind_no_app/client.ts ================================================ import "./assets/style.css"; ================================================ FILE: packages/plugin-vite/tests/fixtures/tailwind_no_app/main.ts ================================================ import { App } from "@fresh/core"; export const app = new App().fsRoutes(); ================================================ FILE: packages/plugin-vite/tests/fixtures/tailwind_no_app/routes/index.tsx ================================================ export default function Hello() { return

ok

; } ================================================ FILE: packages/plugin-vite/tests/fixtures/tailwind_no_app/vite.config.ts ================================================ import { defineConfig } from "vite"; import { fresh } from "@fresh/plugin-vite"; import tailwindcss from "@tailwindcss/vite"; export default defineConfig({ plugins: [fresh(), tailwindcss()], }); ================================================ FILE: packages/plugin-vite/tests/fixtures/test_files_exclusion/deno.json ================================================ { "imports": { "fresh": "jsr:@fresh/core@^2.0.0", "@fresh/plugin-vite": "jsr:@fresh/plugin-vite@^1.0.0", "preact": "npm:preact@^10.27.2", "preact/": "npm:/preact@^10.27.2/" } } ================================================ FILE: packages/plugin-vite/tests/fixtures/test_files_exclusion/main.ts ================================================ import { App } from "fresh"; export const app = new App().fsRoutes(); ================================================ FILE: packages/plugin-vite/tests/fixtures/test_files_exclusion/routes/foo.test.ts ================================================ // This test file should NOT be bundled into production export const handler = () => { return new Response("This should never be in production"); }; ================================================ FILE: packages/plugin-vite/tests/fixtures/test_files_exclusion/routes/index.tsx ================================================ export default function Home() { return
ok
; } ================================================ FILE: packages/plugin-vite/tests/fixtures/test_files_exclusion/routes/index_test.tsx ================================================ // This test file should NOT be bundled into production export default function TestRoute() { return
This should never be in production
; } ================================================ FILE: packages/plugin-vite/tests/fixtures/test_files_exclusion/vite.config.ts ================================================ import { defineConfig } from "vite"; import { fresh } from "@fresh/plugin-vite"; export default defineConfig({ plugins: [fresh()], }); ================================================ FILE: packages/plugin-vite/tests/test_utils.ts ================================================ import { createBuilder } from "vite"; import * as path from "@std/path"; import { walk } from "@std/fs/walk"; import { withTmpDir } from "../../fresh/src/test_utils.ts"; import { withChildProcessServer } from "../../fresh/tests/test_utils.tsx"; export const DEMO_DIR = path.join(import.meta.dirname!, "..", "demo"); export const FIXTURE_DIR = path.join(import.meta.dirname!, "fixtures"); export async function updateFile( filePath: string, fn: (text: string) => string | Promise, ) { const original = await Deno.readTextFile(filePath); const result = await fn(original); await Deno.writeTextFile(filePath, result); return { async [Symbol.asyncDispose]() { await Deno.writeTextFile(filePath, original); }, }; } async function copyDir(from: string, to: string) { const entries = walk(from, { includeFiles: true, includeDirs: false, skip: [/([\\/]+(_fresh|node_modules|vendor)[\\/]+|[\\/]+vite\.config\.ts)/], }); for await (const entry of entries) { if (entry.isFile) { const relative = path.relative(from, entry.path); const target = path.join(to, relative); try { await Deno.mkdir(path.dirname(target), { recursive: true }); } catch (err) { if (!(err instanceof Deno.errors.NotFound)) { throw err; } } await Deno.copyFile(entry.path, target); } } } export async function prepareDevServer(fixtureDir: string) { const tmp = await withTmpDir({ dir: path.join(import.meta.dirname!, ".."), prefix: "tmp_vite_", }); await copyDir(fixtureDir, tmp.dir); await Deno.writeTextFile( path.join(tmp.dir, "vite.config.ts"), `import { defineConfig } from "vite"; import { fresh } from "@fresh/plugin-vite"; export default defineConfig({ plugins: [ fresh(), ], }); `, ); return tmp; } export async function launchDevServer( dir: string, fn: (address: string, dir: string) => void | Promise, env: Record = {}, ) { await withChildProcessServer( { cwd: dir, args: ["run", "-A", "--cached-only", "npm:vite", "--port", "0"], env, }, async (address) => await fn(address, dir), ); } export async function spawnDevServer( dir: string, env: Record = {}, ) { const boot = Promise.withResolvers(); const p = Promise.withResolvers(); let serverAddress = ""; const server = withChildProcessServer( { cwd: dir, args: ["run", "-A", "--cached-only", "npm:vite", "--port", "0"], env, }, async (address) => { serverAddress = address; boot.resolve(); await p.promise; }, ); await boot.promise; return { dir, promise: server, address: () => { return serverAddress; }, async [Symbol.asyncDispose]() { await p.resolve(); }, }; } export async function withDevServer( fixtureDir: string, fn: (address: string, dir: string) => void | Promise, env: Record = {}, ) { await using tmp = await prepareDevServer(fixtureDir); await launchDevServer(tmp.dir, fn, env); } export async function buildVite( fixtureDir: string, options?: { base?: string }, ) { const tmp = await withTmpDir({ dir: path.join(import.meta.dirname!, ".."), prefix: "tmp_vite_", }); const builder = await createBuilder({ logLevel: "error", root: fixtureDir, base: options?.base, build: { emptyOutDir: true, }, environments: { ssr: { build: { outDir: path.join(tmp.dir, "_fresh", "server"), }, }, client: { build: { outDir: path.join(tmp.dir, "_fresh", "client"), }, }, }, }); await builder.buildApp(); return { tmp: tmp.dir, async [Symbol.asyncDispose]() { return await tmp[Symbol.asyncDispose](); }, }; } export function usingEnv(name: string, value: string) { const prev = Deno.env.get(name); Deno.env.set(name, value); return { [Symbol.dispose]: () => { if (prev === undefined) { Deno.env.delete(name); } else { Deno.env.set(name, prev); } }, }; } export interface ProdOptions { cwd: string; args?: string[]; bin?: string; env?: Record; } export async function launchProd( options: ProdOptions, fn: (address: string) => void | Promise, ) { return await withChildProcessServer( { cwd: options.cwd, args: options.args ?? ["serve", "-A", "--cached-only", "--port", "0", "_fresh/server.js"], }, fn, ); } ================================================ FILE: packages/update/README.md ================================================ # Update an existing Fresh project. This is a CLI tool can be used to upgrade an existing Fresh project. To do so, run this command: ```sh deno run -Ar jsr:@fresh/update ``` Go to [https://fresh.deno.dev/](https://fresh.deno.dev/) for more information about Fresh. ================================================ FILE: packages/update/deno.json ================================================ { "name": "@fresh/update", "version": "2.2.1", "license": "MIT", "exports": "./src/mod.ts", "exclude": ["**/tmp/*"], "publish": { "include": [ "src/**/*.ts", "deno.json", "README.md" ], "exclude": ["**/*_test.*", "*.todo"] } } ================================================ FILE: packages/update/src/mod.ts ================================================ import { parseArgs } from "@std/cli/parse-args"; import * as path from "@std/path"; import { ensureMinDenoVersion, error } from "./utils.ts"; import { updateProject } from "./update.ts"; import * as colors from "@std/fmt/colors"; const MIN_DENO_VERSION = "1.43.1"; const HELP = `@fresh/update Update a Fresh project. This updates dependencies and optionally performs code mods to update a project's source code to the latest recommended patterns. To upgrade a project in the current directory, run: deno run -A jsr:@fresh/update . USAGE: @fresh/update [DIRECTORY] `; ensureMinDenoVersion(MIN_DENO_VERSION); const flags = parseArgs(Deno.args, {}); // deno-lint-ignore no-console console.log(colors.bgRgb8( colors.rgb8(" 🍋 Fresh Updater ", 0), 121, )); // deno-lint-ignore no-console console.log(); // deno-lint-ignore no-console console.log( colors.italic( "Note: Breaking changes may require additional manual updates.", ), ); // deno-lint-ignore no-console console.log(); let unresolvedDirectory = Deno.args[0]; if (flags._.length !== 1) { const userInput = prompt("Where is the project directory?", "."); if (!userInput) { error(HELP); } unresolvedDirectory = userInput; } const dir = path.resolve(unresolvedDirectory); await updateProject(dir); ================================================ FILE: packages/update/src/update.ts ================================================ import * as path from "@std/path"; import * as JSONC from "@std/jsonc"; import * as tsmorph from "ts-morph"; import * as colors from "@std/fmt/colors"; import { ProgressBar } from "@std/cli/unstable-progress-bar"; export const SyntaxKind = tsmorph.ts.SyntaxKind; export const FRESH_VERSION = "2.2.1"; export const PREACT_VERSION = "10.28.3"; export const PREACT_SIGNALS_VERSION = "2.7.1"; // Function to filter out node_modules and vendor directories from logs const HIDE_FILES = /[\\/]+(node_modules|vendor)[\\/]+/; export interface DenoJson { lock?: boolean; tasks?: Record; name?: string; version?: string; imports?: Record; } async function format(filePath: string) { const command = new Deno.Command(Deno.execPath(), { args: ["fmt", filePath], }); await command.output(); } async function writeFormatted(filePath: string, content: string) { await Deno.writeTextFile(filePath, content); await format(filePath); } async function updateDenoJson( dir: string, fn: (json: DenoJson) => void | Promise, ): Promise { let filePath = path.join(dir, "deno.json"); try { const config = JSON.parse(await Deno.readTextFile(filePath)) as DenoJson; await fn(config); await writeFormatted(filePath, JSON.stringify(config)); return; } catch (err) { if (!(err instanceof Deno.errors.NotFound)) { throw err; } } filePath = path.join(dir, "deno.jsonc"); try { const config = JSONC.parse(await Deno.readTextFile(filePath)) as DenoJson; await fn(config); await writeFormatted(filePath, JSON.stringify(config)); return; } catch (err) { if (!(err instanceof Deno.errors.NotFound)) { throw err; } } throw new Error(`Could not find deno.json or deno.jsonc in: ${dir}`); } export interface ImportState { core: Set; runtime: Set; compat: Set; httpError: number; } const compat = new Set([ "defineApp", "defineLayout", "defineRoute", "AppProps", "ErrorPageProps", "Handler", "Handlers", "LayoutProps", "RouteContext", "UnknownPageProps", ]); export async function updateProject(dir: string) { // add initial log // deno-lint-ignore no-console console.log(colors.blue("🚀 Starting Fresh 1 to Fresh 2 migration...")); // deno-lint-ignore no-console console.log(colors.yellow("📝 Updating configuration files...")); // Update config await updateDenoJson(dir, (config) => { if (config.imports !== null && typeof config.imports !== "object") { config.imports = {}; } config.imports["fresh"] = `jsr:@fresh/core@^${FRESH_VERSION}`; config.imports["preact"] = `npm:preact@^${PREACT_VERSION}`; config.imports["@preact/signals"] = `npm:@preact/signals@^${PREACT_SIGNALS_VERSION}`; delete config.imports["$fresh/"]; delete config.imports["@preact/signals-core"]; delete config.imports["preact-render-to-string"]; // We should always use a lockfile going forwards if ("lock" in config) { delete config.lock; } // Update Fresh 1.x tasks const tasks = config.tasks; if (tasks !== undefined) { if (tasks.manifest === "deno task cli manifest $(pwd)") { delete tasks.manifest; } if ( tasks.cli === "echo \"import '\\$fresh/src/dev/cli.ts'\" | deno run --unstable -A -" || tasks.cli === "echo \"import '$fresh/src/dev/cli.ts'\" | deno run --unstable -A -" ) { delete tasks.cli; } if (tasks.update === "deno run -A -r https://fresh.deno.dev/update .") { tasks.update = "deno run -A -r jsr:@fresh/update ."; } if ( tasks.check === "deno fmt --check && deno lint && deno check **/*.ts && deno check **/*.tsx" ) { tasks.check = "deno fmt --check && deno lint && deno check"; } if (tasks.preview === "deno run -A main.ts") { tasks.preview = "deno serve -A _fresh/server.js"; } } }); // add completion log // deno-lint-ignore no-console console.log(colors.green("✅ Configuration updated successfully")); // Delete fresh.gen.ts if it exists (no longer needed in Fresh 2.0) const freshGenPath = path.join(dir, "fresh.gen.ts"); try { await Deno.remove(freshGenPath); // deno-lint-ignore no-console console.log(colors.cyan("🗑️ Deleted fresh.gen.ts (no longer needed)")); } catch (err) { if (!(err instanceof Deno.errors.NotFound)) { // deno-lint-ignore no-console console.error( `Could not delete fresh.gen.ts: ${ err instanceof Error ? err.message : String(err) }`, ); } // If file doesn't exist, that's fine - nothing to do } // deno-lint-ignore no-console console.log(colors.cyan("🔍 Scanning for source files...")); // Update routes folder const project = new tsmorph.Project(); const sfs = project.addSourceFilesAtPaths( path.join(dir, "**", "*.{js,jsx,ts,tsx}"), ); // Filter out node_modules and vendor files for user display const userFiles = sfs.filter((sf) => !HIDE_FILES.test(sf.getFilePath())); // deno-lint-ignore no-console console.log(colors.cyan(`📁 Found ${userFiles.length} files to process`)); if (sfs.length === 0) { // deno-lint-ignore no-console console.log(colors.green("🎉 Migration completed successfully!")); return; } // deno-lint-ignore no-console console.log(colors.yellow("🔄 Processing files...")); // Track progress with a single counter let completedFiles = 0; const modifiedFilesList: string[] = []; // Create a progress bar const bar = new ProgressBar({ max: sfs.length, formatter(x) { return `[${x.styledTime}] [${x.progressBar}] [${x.value}/${x.max} files]`; }, }); // process files sequentially to show proper progress await Promise.all(sfs.map(async (sourceFile) => { try { const wasModified = await updateFile(sourceFile); if (wasModified) { modifiedFilesList.push(sourceFile.getFilePath()); } return wasModified; } catch (err) { // deno-lint-ignore no-console console.error(`Could not process ${sourceFile.getFilePath()}`); throw err; } finally { completedFiles++; bar.value = completedFiles; } })); // Clear the progress line and add a newline await bar.stop(); // Filter modified files to show only user files const modifiedFilesToShow = modifiedFilesList.filter((filePath) => !HIDE_FILES.test(filePath) ); // add migration summary // deno-lint-ignore no-console console.log("\n" + colors.bold("📊 Migration Summary:")); // deno-lint-ignore no-console console.log(` Total files processed: ${userFiles.length}`); // deno-lint-ignore no-console console.log(` Successfully modified: ${modifiedFilesToShow.length}`); // deno-lint-ignore no-console console.log( ` Unmodified (no changes needed): ${ userFiles.length - modifiedFilesToShow.length }`, ); // Display modified files list (filtered) if (modifiedFilesToShow.length > 0) { // deno-lint-ignore no-console console.log("\n" + colors.bold("📝 Modified Files:")); modifiedFilesToShow.forEach((filePath) => { // show relative path let relativePath = path.relative(dir, filePath); // Ensure consistent path separators for logging relativePath = relativePath.replace(/\\/g, "/"); // deno-lint-ignore no-console console.log(colors.green(` ✓ ${relativePath}`)); }); } // deno-lint-ignore no-console console.log("\n" + colors.green("🎉 Migration completed successfully!")); } async function updateFile(sourceFile: tsmorph.SourceFile): Promise { // keep original text for comparison const originalText = sourceFile.getFullText(); const newImports: ImportState = { core: new Set(), runtime: new Set(), compat: new Set(), httpError: 0, }; const text = sourceFile.getFullText() .replaceAll("/** @jsx h */\n", "") .replaceAll("/** @jsxFrag Fragment */\n", "") .replaceAll('/// \n', "") .replaceAll('/// \n', "") .replaceAll('/// \n', "") .replaceAll('/// \n', "") .replaceAll('/// \n', ""); sourceFile.replaceWithText(text); if ( sourceFile.getFilePath().includes("/routes/") && !sourceFile.getDirectoryPath().includes("/(_") ) { for (const [name, decl] of sourceFile.getExportedDeclarations()) { if (name === "handler") { const node = decl[0]; if (node.isKind(SyntaxKind.VariableDeclaration)) { const init = node.getInitializer(); if ( init !== undefined && init.isKind(SyntaxKind.ObjectLiteralExpression) ) { for (const property of init.getProperties()) { if (property.isKind(SyntaxKind.MethodDeclaration)) { const name = property.getName(); if ( name === "GET" || name === "POST" || name === "PATCH" || name === "PUT" || name === "DELETE" ) { const body = property.getBody(); if (body !== undefined) { const stmts = body.getDescendantStatements(); rewriteCtxMethods(newImports, stmts); } maybePrependReqVar(property, newImports, true); } } else if (property.isKind(SyntaxKind.PropertyAssignment)) { const init = property.getInitializer(); if ( init !== undefined && (init.isKind(SyntaxKind.ArrowFunction) || init.isKind(SyntaxKind.FunctionExpression)) ) { const body = init.getBody(); if (body !== undefined) { const stmts = body.getDescendantStatements(); rewriteCtxMethods(newImports, stmts); } maybePrependReqVar(init, newImports, true); } } } } } else if (node.isKind(SyntaxKind.FunctionDeclaration)) { const body = node.getBody(); if (body !== undefined) { const stmts = body.getDescendantStatements(); rewriteCtxMethods(newImports, stmts); } maybePrependReqVar(node, newImports, false); } } else if (name === "default" && decl.length > 0) { const caller = decl[0]; if (caller.isKind(SyntaxKind.CallExpression)) { const expr = caller.getExpression(); if (expr.isKind(SyntaxKind.Identifier)) { const text = expr.getText(); if ( text === "defineApp" || text === "defineLayout" || text === "defineRoute" ) { const args = caller.getArguments(); if (args.length > 0) { const first = args[0]; if ( first.isKind(SyntaxKind.ArrowFunction) || first.isKind(SyntaxKind.FunctionExpression) ) { const body = first.getBody(); if (body !== undefined) { const stmts = body.getDescendantStatements(); rewriteCtxMethods(newImports, stmts); } maybePrependReqVar(first, newImports, false); } } } } } else if (caller.isKind(SyntaxKind.FunctionDeclaration)) { const body = caller.getBody(); if (body !== undefined) { const stmts = body.getDescendantStatements(); rewriteCtxMethods(newImports, stmts); } maybePrependReqVar(caller, newImports, false); } } } } let hasCoreImport = false; let hasRuntimeImport = false; for (const d of sourceFile.getImportDeclarations()) { const specifier = d.getModuleSpecifierValue(); if (specifier === "preact") { for (const n of d.getNamedImports()) { const name = n.getName(); if (name === "h" || name === "Fragment") n.remove(); } removeEmptyImport(d); } else if (specifier === "$fresh/server.ts") { hasCoreImport = true; d.setModuleSpecifier("fresh"); for (const n of d.getNamedImports()) { const name = n.getName(); newImports.core.delete(name); if (compat.has(name)) { n.remove(); newImports.compat.add(name); } } if (newImports.core.size > 0) { newImports.core.forEach((name) => { d.addNamedImport(name); }); } removeEmptyImport(d); } else if (specifier === "$fresh/runtime.ts") { hasRuntimeImport = true; d.setModuleSpecifier("fresh/runtime"); for (const n of d.getNamedImports()) { const name = n.getName(); newImports.runtime.delete(name); } if (newImports.runtime.size > 0) { newImports.runtime.forEach((name) => { d.addNamedImport(name); }); } removeEmptyImport(d); } } if (!hasCoreImport && newImports.core.size > 0) { sourceFile.addImportDeclaration({ moduleSpecifier: "fresh", namedImports: Array.from(newImports.core), }); } if (!hasRuntimeImport && newImports.runtime.size > 0) { sourceFile.addImportDeclaration({ moduleSpecifier: "fresh/runtime", namedImports: Array.from(newImports.runtime), }); } if (newImports.compat.size > 0) { sourceFile.addImportDeclaration({ moduleSpecifier: "fresh/compat", namedImports: Array.from(newImports.compat), }); } if (newImports.httpError > 0) { sourceFile.addImportDeclaration({ moduleSpecifier: "fresh", namedImports: ["HttpError"], }); } await sourceFile.save(); await format(sourceFile.getFilePath()); // Check if the file was actually modified const finalText = sourceFile.getFullText(); return originalText !== finalText; } function removeEmptyImport(d: tsmorph.ImportDeclaration) { if ( d.getNamedImports().length === 0 && d.getNamespaceImport() === undefined && d.getDefaultImport() === undefined ) { d.remove(); } } function maybePrependReqVar( method: | tsmorph.MethodDeclaration | tsmorph.FunctionDeclaration | tsmorph.FunctionExpression | tsmorph.ArrowFunction, newImports: ImportState, hasInferredTypes: boolean, ) { let hasRequestVar = false; const params = method.getParameters(); if (params.length > 0) { const paramName = params[0].getName(); // Add explicit types if the user did that if (hasInferredTypes && params[0].getTypeNode()) { hasInferredTypes = false; } hasRequestVar = params.length > 1 || paramName === "req"; if (hasRequestVar || paramName === "_req") { if (hasRequestVar && params.length === 1) { params[0].replaceWithText("ctx"); if (!hasInferredTypes) { newImports.core.add("FreshContext"); params[0].setType("FreshContext"); } } else { params[0].remove(); // Use proper type if (params.length > 1) { const initType = params[1].getTypeNode()?.getText(); if (initType !== undefined && initType === "RouteContext") { newImports.core.add("FreshContext"); params[1].setType("FreshContext"); } } } } const maybeObjBinding = params.length > 1 ? params[1].getNameNode() : undefined; if (method.isKind(SyntaxKind.ArrowFunction)) { const body = method.getBody(); if (!body.isKind(SyntaxKind.Block)) { // deno-lint-ignore no-console console.warn(`Cannot transform arrow function`); return; } } if ( (maybeObjBinding === undefined || !maybeObjBinding.isKind(SyntaxKind.ObjectBindingPattern)) && hasRequestVar && !paramName.startsWith("_") ) { method.insertVariableStatement(0, { declarationKind: tsmorph.VariableDeclarationKind.Const, declarations: [{ name: paramName, initializer: "ctx.req", }], }); } if ( maybeObjBinding !== undefined && maybeObjBinding.isKind(SyntaxKind.ObjectBindingPattern) ) { const bindings = maybeObjBinding.getElements(); if (bindings.length > 0) { let needsRemoteAddr = false; for (let i = 0; i < bindings.length; i++) { const binding = bindings[i]; const name = binding.getName(); if (name === "remoteAddr") { binding.replaceWithText("info"); needsRemoteAddr = true; } } if (hasRequestVar && !paramName.startsWith("_")) { const txt = maybeObjBinding.getFullText().slice(0, -2); maybeObjBinding.replaceWithText(txt + ", req }"); } if (needsRemoteAddr) { method.insertVariableStatement(0, { declarationKind: tsmorph.VariableDeclarationKind.Const, declarations: [{ name: "remoteAddr", initializer: "info.remoteAddr", }], }); } } } } } function rewriteCtxMethods( importState: ImportState, nodes: (tsmorph.Node)[], ) { for (let i = 0; i < nodes.length; i++) { const node = nodes[i]; if (node.wasForgotten()) continue; if ( node.isKind(SyntaxKind.ExpressionStatement) && node.getText() === "ctx.renderNotFound();" ) { importState.httpError++; node.replaceWithText("throw new HttpError(404);"); continue; } if (node.isKind(SyntaxKind.PropertyAccessExpression)) { rewriteCtxMemberName(node); } else if (node.isKind(SyntaxKind.ReturnStatement)) { if (node.getText() === "return ctx.renderNotFound();") { importState.httpError++; node.replaceWithText(`throw new HttpError(404)`); continue; } else { const expr = node.getExpression(); if (expr !== undefined) { rewriteCtxMethods(importState, [expr]); } } } else if (node.isKind(SyntaxKind.VariableStatement)) { const decls = node.getDeclarations(); for (let i = 0; i < decls.length; i++) { const decl = decls[i]; const init = decl.getInitializer(); if (init !== undefined) { rewriteCtxMethods(importState, [init]); } } } else if ( node.isKind(SyntaxKind.ExpressionStatement) || node.isKind(SyntaxKind.AwaitExpression) || node.isKind(SyntaxKind.CallExpression) ) { const expr = node.getExpression(); rewriteCtxMethods(importState, [expr]); } else if (node.isKind(SyntaxKind.BinaryExpression)) { rewriteCtxMethods(importState, [node.getLeft()]); rewriteCtxMethods(importState, [node.getRight()]); } else if ( !node.isKind(SyntaxKind.ExpressionStatement) && node.getKindName().endsWith("Statement") ) { const inner = node.getDescendantStatements(); rewriteCtxMethods(importState, inner); } } } function rewriteCtxMemberName( node: tsmorph.PropertyAccessExpression, ) { const children = node.getChildren(); if (children.length === 0) return; if ( node.getExpression().getText() === "ctx" && node.getName() === "remoteAddr" ) { node.getExpression().replaceWithText("ctx.info.remoteAddr"); } else if (children[0].isKind(SyntaxKind.PropertyAccessExpression)) { rewriteCtxMemberName(children[0]); } } ================================================ FILE: packages/update/src/update_test.ts ================================================ import * as path from "@std/path"; import { FRESH_VERSION, PREACT_SIGNALS_VERSION, PREACT_VERSION, updateProject, } from "./update.ts"; import { expect } from "@std/expect"; import { spy, type SpyCall } from "@std/testing/mock"; import { walk } from "@std/fs/walk"; import { withTmpDir, writeFiles } from "../../fresh/src/test_utils.ts"; async function readFiles(dir: string): Promise> { const files: Record = {}; for await ( const entry of walk(dir, { includeDirs: false, includeFiles: true }) ) { const pathname = path.relative(dir, entry.path); const content = await Deno.readTextFile(entry.path); files[`/${pathname.replaceAll(/[\\]+/g, "/")}`] = content.trim(); } return files; } Deno.test("update - remove JSX pragma import", async () => { await using _tmp = await withTmpDir(); const dir = _tmp.dir; await writeFiles(dir, { "/deno.json": `{}`, "/routes/index.tsx": `import { h, Fragment } from "preact"; /** @jsx h */ /** @jsxFrag Fragment */ export default function Foo() { return null; }`, }); await updateProject(dir); const files = await readFiles(dir); expect(files["/routes/index.tsx"]) .toEqual(`export default function Foo() { return null; }`); }); Deno.test("update - 1.x project deno.json", async () => { await using _tmp = await withTmpDir(); const dir = _tmp.dir; await writeFiles(dir, { "/deno.json": `{}`, }); await updateProject(dir); const files = await readFiles(dir); expect(JSON.parse(files["/deno.json"])) .toEqual({ imports: { "fresh": `jsr:@fresh/core@^${FRESH_VERSION}`, "@preact/signals": `npm:@preact/signals@^${PREACT_SIGNALS_VERSION}`, "preact": `npm:preact@^${PREACT_VERSION}`, }, }); }); Deno.test("update - 1.x project deno.json with imports", async () => { await using _tmp = await withTmpDir(); const dir = _tmp.dir; await writeFiles(dir, { "/deno.json": `{ "imports": { "$fresh/": "foo" } }`, }); await updateProject(dir); const files = await readFiles(dir); expect(JSON.parse(files["/deno.json"])) .toEqual({ imports: { "fresh": `jsr:@fresh/core@^${FRESH_VERSION}`, "@preact/signals": `npm:@preact/signals@^${PREACT_SIGNALS_VERSION}`, "preact": `npm:preact@^${PREACT_VERSION}`, }, }); }); Deno.test("update - 1.x project deno.json tasks + lock", async () => { await using tmp = await withTmpDir(); await writeFiles(tmp.dir, { "/deno.json": `{ "lock": false, "tasks": { "check": "deno fmt --check && deno lint && deno check **/*.ts && deno check **/*.tsx", "cli": "echo \\"import '$fresh/src/dev/cli.ts'\\" | deno run --unstable -A -", "manifest": "deno task cli manifest $(pwd)", "start": "deno run -A --watch=static/,routes/ dev.ts", "build": "deno run -A dev.ts build", "preview": "deno run -A main.ts", "update": "deno run -A -r https://fresh.deno.dev/update ." } }`, }); await updateProject(tmp.dir); const files = await readFiles(tmp.dir); const updated = JSON.parse(files["/deno.json"]); expect(updated.lock).toEqual(undefined); expect(updated.tasks) .toEqual({ build: "deno run -A dev.ts build", check: "deno fmt --check && deno lint && deno check", preview: "deno serve -A _fresh/server.js", start: "deno run -A --watch=static/,routes/ dev.ts", update: "deno run -A -r jsr:@fresh/update .", }); }); Deno.test("update - 1.x project middlewares", async () => { await using _tmp = await withTmpDir(); const dir = _tmp.dir; await writeFiles(dir, { "/deno.json": "{}", "/routes/_middleware.ts": `import { FreshContext } from "$fresh/server.ts"; interface State { data: string; } export async function handler( req: Request, ctx: FreshContext, ) { ctx.state.data = "myData"; ctx.state.url = req.url; const resp = await ctx.next(); resp.headers.set("server", "fresh server"); return resp; }`, }); await updateProject(dir); const files = await readFiles(dir); expect(files["/routes/_middleware.ts"]) .toEqual(`import { FreshContext } from "fresh"; interface State { data: string; } export async function handler( ctx: FreshContext, ) { const req = ctx.req; ctx.state.data = "myData"; ctx.state.url = req.url; const resp = await ctx.next(); resp.headers.set("server", "fresh server"); return resp; }`); }); Deno.test("update - 1.x project middlewares one arg", async () => { await using _tmp = await withTmpDir(); const dir = _tmp.dir; await writeFiles(dir, { "/deno.json": "{}", "/routes/_middleware.ts": `export async function handler(req: Request) { return new Response("hello world from: " + req.url); }`, }); await updateProject(dir); const files = await readFiles(dir); expect(files["/routes/_middleware.ts"]) .toEqual(`import { FreshContext } from "fresh"; export async function handler(ctx: FreshContext) { const req = ctx.req; return new Response("hello world from: " + req.url); }`); }); Deno.test("update - 1.x update '$fresh/*' imports", async () => { await using _tmp = await withTmpDir(); const dir = _tmp.dir; await writeFiles(dir, { "/deno.json": `{}`, "/routes/index.tsx": `import { PageProps } from "$fresh/server.ts"; export default function Foo(props: PageProps) { return null; }`, "/routes/foo.tsx": `import { asset, Head } from "$fresh/runtime.ts";`, }); await updateProject(dir); const files = await readFiles(dir); expect(files["/routes/index.tsx"]) .toEqual(`import { PageProps } from "fresh"; export default function Foo(props: PageProps) { return null; }`); expect(files["/routes/foo.tsx"]) .toEqual(`import { asset, Head } from "fresh/runtime";`); }); Deno.test("update - 1.x update handler signature", async () => { await using _tmp = await withTmpDir(); const dir = _tmp.dir; await writeFiles(dir, { "/deno.json": `{}`, "/routes/index.tsx": `import { Handlers } from "$fresh/server.ts"; export const handler: Handlers = { async GET(req, ctx) {}, async POST(req, ctx) {}, async PATCH(req, ctx) {}, async PUT(req, ctx) {}, async DELETE(req, ctx) {}, };`, "/routes/foo.tsx": `import { Handlers } from "$fresh/server.ts"; export const handler: Handlers = { async GET(_req, ctx) {}, async POST(_req, ctx) {}, async PATCH(_req, ctx) {}, async PUT(_req, ctx) {}, async DELETE(_req, ctx) {}, };`, "/routes/name.tsx": `import { Handlers } from "$fresh/server.ts"; export const handler: Handlers = { async GET(request, ctx) {}, async POST(request, ctx) {}, async PATCH(request, ctx) {}, async PUT(request, ctx) {}, async DELETE(request, ctx) {}, };`, "/routes/name-unused.tsx": `import { Handlers } from "$fresh/server.ts"; export const handler: Handlers = { async GET(_request, ctx) {}, async POST(_request, ctx) {}, async PATCH(_request, ctx) {}, async PUT(_request, ctx) {}, async DELETE(_request, ctx) {}, };`, }); await updateProject(dir); const files = await readFiles(dir); expect(files["/routes/index.tsx"]) .toEqual(`import { Handlers } from "fresh/compat"; export const handler: Handlers = { async GET(ctx) { const req = ctx.req; }, async POST(ctx) { const req = ctx.req; }, async PATCH(ctx) { const req = ctx.req; }, async PUT(ctx) { const req = ctx.req; }, async DELETE(ctx) { const req = ctx.req; }, };`); expect(files["/routes/foo.tsx"]) .toEqual(`import { Handlers } from "fresh/compat"; export const handler: Handlers = { async GET(ctx) {}, async POST(ctx) {}, async PATCH(ctx) {}, async PUT(ctx) {}, async DELETE(ctx) {}, };`); expect(files["/routes/name.tsx"]) .toEqual(`import { Handlers } from "fresh/compat"; export const handler: Handlers = { async GET(ctx) { const request = ctx.req; }, async POST(ctx) { const request = ctx.req; }, async PATCH(ctx) { const request = ctx.req; }, async PUT(ctx) { const request = ctx.req; }, async DELETE(ctx) { const request = ctx.req; }, };`); expect(files["/routes/name-unused.tsx"]) .toEqual(`import { Handlers } from "fresh/compat"; export const handler: Handlers = { async GET(ctx) {}, async POST(ctx) {}, async PATCH(ctx) {}, async PUT(ctx) {}, async DELETE(ctx) {}, };`); }); Deno.test( "update - 1.x update handler signature method one arg", async () => { await using _tmp = await withTmpDir(); const dir = _tmp.dir; await writeFiles(dir, { "/deno.json": `{}`, "/routes/index.tsx": `export const handler: Handlers = { GET(req) { return Response.redirect(req.url); }, };`, }); await updateProject(dir); const files = await readFiles(dir); expect(files["/routes/index.tsx"]) .toEqual(`export const handler: Handlers = { GET(ctx) { const req = ctx.req; return Response.redirect(req.url); }, };`); }, ); Deno.test.ignore( "update - 1.x update handler signature variable", async () => { await using _tmp = await withTmpDir(); const dir = _tmp.dir; await writeFiles(dir, { "/deno.json": `{}`, "/routes/index.tsx": `export const handler: Handlers = { GET: (req) => Response.redirect(req.url) };`, "/routes/foo.tsx": `export const handler: Handlers = { GET: (req, ctx) => Response.redirect(req.url), };`, }); await updateProject(dir); const files = await readFiles(dir); expect(files["/routes/index.tsx"]) .toEqual(`export const handler: Handlers = { GET: (ctx) => { const req = ctx.req; return Response.redirect(req.url); }, };`); expect(files["/routes/foo.tsx"]) .toEqual(`export const handler: Handlers = { GET: (ctx) => { const req = ctx.req; return Response.redirect(req.url); }, };`); }, ); Deno.test( "update - 1.x update handler signature non-inferred", async () => { await using _tmp = await withTmpDir(); const dir = _tmp.dir; await writeFiles(dir, { "/deno.json": `{}`, "/routes/index.tsx": `export const handler = { GET(req: Request){ return Response.redirect(req.url); } };`, }); await updateProject(dir); const files = await readFiles(dir); expect(files["/routes/index.tsx"]) .toEqual(`import { FreshContext } from "fresh"; export const handler = { GET(ctx: FreshContext) { const req = ctx.req; return Response.redirect(req.url); }, };`); }, ); Deno.test( "update - 1.x update handler signature with destructure", async () => { await using _tmp = await withTmpDir(); const dir = _tmp.dir; await writeFiles(dir, { "/deno.json": `{}`, "/routes/index.tsx": `import { Handlers } from "$fresh/server.ts"; export const handler: Handlers = { async GET(req, { params, render, remoteAddr }) {}, async POST(req, { params, render, remoteAddr }) {}, async PATCH(req, { params, render, remoteAddr }) {}, async PUT(req, { params, render, remoteAddr }) {}, async DELETE(req, { params, render, remoteAddr }) {}, };`, }); await updateProject(dir); const files = await readFiles(dir); expect(files["/routes/index.tsx"]) .toEqual(`import { Handlers } from "fresh/compat"; export const handler: Handlers = { async GET({ params, render, info, req }) { const remoteAddr = info.remoteAddr; }, async POST({ params, render, info, req }) { const remoteAddr = info.remoteAddr; }, async PATCH({ params, render, info, req }) { const remoteAddr = info.remoteAddr; }, async PUT({ params, render, info, req }) { const remoteAddr = info.remoteAddr; }, async DELETE({ params, render, info, req }) { const remoteAddr = info.remoteAddr; }, };`); }, ); Deno.test("update - 1.x update define* handler signatures", async () => { await using _tmp = await withTmpDir(); const dir = _tmp.dir; await writeFiles(dir, { "/deno.json": `{}`, "/routes/_app.tsx": `import { defineApp } from "$fresh/server.ts"; export default defineApp(async (req, ctx) => { return null; });`, "/routes/_layout.tsx": `import { defineLayout } from "$fresh/server.ts"; export default defineLayout(async (req, ctx) => { return null; });`, "/routes/foo.tsx": `import { defineRoute } from "$fresh/server.ts"; export default defineRoute(async (req, ctx) => { return null; });`, }); await updateProject(dir); const files = await readFiles(dir); expect(files["/routes/_app.tsx"]) .toEqual(`import { defineApp } from "fresh/compat"; export default defineApp(async (ctx) => { const req = ctx.req; return null; });`); expect(files["/routes/_layout.tsx"]) .toEqual(`import { defineLayout } from "fresh/compat"; export default defineLayout(async (ctx) => { const req = ctx.req; return null; });`); expect(files["/routes/foo.tsx"]) .toEqual(`import { defineRoute } from "fresh/compat"; export default defineRoute(async (ctx) => { const req = ctx.req; return null; });`); }); Deno.test( "update - 1.x update component signature async", async () => { await using _tmp = await withTmpDir(); const dir = _tmp.dir; await writeFiles(dir, { "/deno.json": `{}`, "/routes/index.tsx": `export default async function Index(req: Request, ctx: RouteContext) { if (true) { return ctx.renderNotFound(); } if ("foo" === "foo" as any) { ctx.renderNotFound(); return ctx.renderNotFound(); } return new Response(req.url); }`, }); await updateProject(dir); const files = await readFiles(dir); expect(files["/routes/index.tsx"]) .toEqual(`import { FreshContext } from "fresh"; import { HttpError } from "fresh"; export default async function Index(ctx: FreshContext) { const req = ctx.req; if (true) { throw new HttpError(404); } if ("foo" === "foo" as any) { throw new HttpError(404); throw new HttpError(404); } return new Response(req.url); }`); }, ); Deno.test.ignore( "update - 1.x ctx.renderNotFound() -> throw new HttpError(404)", async () => { await using _tmp = await withTmpDir(); const dir = _tmp.dir; await writeFiles(dir, { "/deno.json": `{}`, "/routes/index.tsx": `import { Handlers } from "$fresh/server.ts"; export const handler: Handlers = { async GET(_req, ctx) { return ctx.renderNotFound(); }, };`, "/routes/foo.tsx": `export const handler = (ctx) => { return ctx.renderNotFound(); }`, }); await updateProject(dir); const files = await readFiles(dir); expect(files["/routes/index.tsx"]) .toEqual(`import { Handlers } from "fresh"; export const handler: Handlers = { async GET(ctx) { throw HttpError(404); }, };`); expect(files["/routes/foo.tsx"]) .toEqual(`export const handler = (ctx) => { throw HttpError(404); };`); }, ); Deno.test.ignore( "update - 1.x ctx.remoteAddr -> ctx.info.remoteAddr", async () => { await using _tmp = await withTmpDir(); const dir = _tmp.dir; await writeFiles(dir, { "/deno.json": `{}`, "/routes/index.tsx": `import { Handlers } from "$fresh/server.ts"; export const handler: Handlers = { async GET(_req, ctx) { let msg = ctx.remoteAddr.transport === "tcp" ? "ok" : "not ok"; msg += typeof ctx.renderNotFound === "function"; return new Response(msg); }, };`, }); await updateProject(dir); const files = await readFiles(dir); expect(files["/routes/index.tsx"]) .toEqual(`import { Handlers } from "fresh"; export const handler: Handlers = { async GET(ctx) { let msg = ctx.info.remoteAddr.transport === "tcp" ? "ok" : "not ok"; msg += typeof ctx.throw === "function"; return new Response(msg); }, };`); }, ); Deno.test.ignore("update - 1.x destructured ctx members", async () => { await using _tmp = await withTmpDir(); const dir = _tmp.dir; await writeFiles(dir, { "/deno.json": `{}`, "/routes/index.tsx": `import { Handlers } from "$fresh/server.ts"; export const handler: Handlers = { async GET(_req, { url, renderNotFound, remoteAddr }) { if (true) { return new Response(!!remoteAddr ? "ok" : "not ok"); } else { console.log(url.href); return renderNotFound(); } }, };`, }); await updateProject(dir); const files = await readFiles(dir); expect(files["/routes/index.tsx"]) .toEqual(`import { Handlers } from "fresh"; export const handler: Handlers = { async GET({ url, throw, info }) { const renderNotFound = () => throw(404); const remoteAddr = info.remoteAddr; if (true) { return new Response(!!remoteAddr ? "ok" : "not ok"); } else { console.log(url.href); return renderNotFound(); } }, };`); }); Deno.test("update - 1.x remove reference comments", async () => { await using _tmp = await withTmpDir(); const dir = _tmp.dir; await writeFiles(dir, { "/deno.json": `{}`, "/routes/main.ts": `/// /// /// /// /// `, }); await updateProject(dir); const files = await readFiles(dir); expect(files["/routes/main.ts"]).toEqual(""); }); Deno.test("update - island files", async () => { await using _tmp = await withTmpDir(); const dir = _tmp.dir; await writeFiles(dir, { "/deno.json": `{}`, "/islands/foo.tsx": `import { IS_BROWSER } from "$fresh/runtime.ts";`, }); await updateProject(dir); const files = await readFiles(dir); expect(files["/islands/foo.tsx"]).toEqual( `import { IS_BROWSER } from "fresh/runtime";`, ); }); Deno.test("update - ignores node_modules and vendor in logs", async () => { await using _tmp = await withTmpDir(); const dir = _tmp.dir; await writeFiles(dir, { "/deno.json": `{}`, "/routes/index.tsx": `import { PageProps } from "$fresh/server.ts"; export default function Foo(props: PageProps) { return null; }`, "/node_modules/foo/bar.ts": `import { IS_BROWSER } from "$fresh/runtime.ts";`, "/vendor/foo/bar.ts": `import { IS_BROWSER } from "$fresh/runtime.ts";`, }); const consoleLogSpy = spy(console, "log"); try { await updateProject(dir); } finally { consoleLogSpy.restore(); } const files = await readFiles(dir); expect(files["/node_modules/foo/bar.ts"]).toEqual( `import { IS_BROWSER } from "fresh/runtime";`, ); expect(files["/vendor/foo/bar.ts"]).toEqual( `import { IS_BROWSER } from "fresh/runtime";`, ); expect(files["/routes/index.tsx"]).toEqual( `import { PageProps } from "fresh"; export default function Foo(props: PageProps) { return null; }`, ); const fullLog = consoleLogSpy.calls.map((call: SpyCall) => call.args.join(" ") ).join( "\n", ); expect(fullLog).toMatch(/Total files processed: 1/); expect(fullLog).toMatch(/Successfully modified: 1/); expect(fullLog).toMatch(/Unmodified \(no changes needed\): 0/); expect(fullLog).not.toMatch(/node_modules/); expect(fullLog).not.toMatch(/vendor/); expect(fullLog).toMatch(/✓ routes\/index.tsx/); }); ================================================ FILE: packages/update/src/utils.ts ================================================ import * as semver from "@std/semver"; /** * Check that the minimum supported Deno version is being used. */ export function ensureMinDenoVersion(minVersion: string) { if ( !semver.greaterOrEqual( semver.parse(Deno.version.deno), semver.parse(minVersion), ) ) { let message = `Deno version ${minVersion} or higher is required. Please update Deno.\n\n`; if (Deno.execPath().includes("homebrew")) { message += "You seem to have installed Deno via homebrew. To update, run: `brew upgrade deno`\n"; } else { message += "To update, run: `deno upgrade`\n"; } error(message); } } export function error(message: string): never { // deno-lint-ignore no-console console.error(`%cerror%c: ${message}`, "color: red; font-weight: bold", ""); Deno.exit(1); } ================================================ FILE: tools/check_docs.ts ================================================ import { checkDocs } from "https://github.com/denoland/std/raw/refs/heads/main/_tools/check_docs.ts"; await checkDocs([ import.meta.resolve("../packages/fresh/src/error.ts"), ]); ================================================ FILE: tools/check_links.ts ================================================ import { DOMParser } from "linkedom"; import * as path from "@std/path"; import { launchProd } from "../packages/plugin-vite/tests/test_utils.ts"; import { createBuilder } from "vite"; const www = path.join(import.meta.dirname!, "..", "www"); const builder = await createBuilder({ root: www, }); await builder.buildApp(); interface CheckLink { url: URL; referrer: URL | null; } await launchProd({ cwd: www }, async (address) => { const first = new URL(address); const stack: CheckLink[] = [{ referrer: null, url: first }]; const seen = new Set(); let current: CheckLink | undefined; while ((current = stack.pop()) !== undefined) { seen.add(current.url.pathname); // deno-lint-ignore no-console console.log("Checking...", current.url.href); const headers = new Headers(); headers.set( "accept", "text/html, application/xhtml+xml, application/xml;q=0.9, image/webp, */*;q=0.8", ); const res = await fetch(current.url, { headers }); const text = await res.text(); if (res.status === 404) { throw new Error( `Failed url ${current.url.href}, referrer: ${current.referrer?.href}`, ); } if (!res.headers.get("Content-type")?.includes("text/html")) { continue; } const doc = new DOMParser().parseFromString(text, "text/html"); for (const link of doc.querySelectorAll("a")) { const next = new URL(link.href, first.origin); if (next.origin !== first.origin) continue; if (seen.has(next.pathname)) continue; stack.push({ url: next, referrer: current.url }); } } }); ================================================ FILE: tools/release.ts ================================================ import * as cl from "@std/fmt/colors"; import * as path from "@std/path"; import type { DenoJson } from "../packages/update/src/update.ts"; import * as semver from "@std/semver"; function showHelp() { // deno-lint-ignore no-console console.log(` Usage: deno run -A release.ts `); } function exitError(msg: string): never { // deno-lint-ignore no-console console.error(cl.red(msg)); showHelp(); Deno.exit(1); } if (Deno.args.length === 0) { exitError(`Missing version argument.`); } else if (Deno.args.length > 1) { exitError(`Too many arguments. Expected only one release argument`); } const ROOT_DIR = path.join(import.meta.dirname!, ".."); const denoJsonPath = path.join(ROOT_DIR, "packages", "fresh", "deno.json"); const wwwDenoJsonPath = path.join(ROOT_DIR, "www", "deno.json"); const denoJson = JSON.parse(await Deno.readTextFile(denoJsonPath)) as DenoJson; const version = Deno.args[0]; const current = semver.parse(denoJson.version!); const next = semver.parse(denoJson.version!); if (version === "major") { next.major++; next.minor = 0; next.patch = 0; next.prerelease = undefined; } else if (version === "minor") { next.minor++; next.patch = 0; } else if (version === "patch") { next.patch++; next.prerelease = undefined; } else { if (!next.prerelease) { exitError(`Unknown prerelease version`); } if (next.prerelease[0] === version) { ((next.prerelease[1]) as number)++; } else { next.prerelease[0] = version; next.prerelease[1] = 1; } } const updateJsonPath = path.join(ROOT_DIR, "packages", "update", "deno.json"); const updateJson = JSON.parse(await Deno.readTextFile(updateJsonPath)); const currentUpdate = semver.parse(updateJson.version); function formatUpgradeMsg( name: string, from: semver.SemVer, to: semver.SemVer, ): string { const nameMsg = cl.yellow(name); const fromMsg = cl.green(semver.format(from)); const toMsg = cl.yellow(semver.format(to)); return ` ${nameMsg}: ${fromMsg} -> ${toMsg}`; } // deno-lint-ignore no-console console.log(formatUpgradeMsg(denoJson.name!, current, next)); // deno-lint-ignore no-console console.log(formatUpgradeMsg(updateJson.name!, currentUpdate, next)); if (!confirm("Proceed with update?")) { Deno.exit(0); } const denoTailwindJson = JSON.parse( await Deno.readTextFile( path.join(ROOT_DIR, "packages", "plugin-tailwindcss", "deno.json"), ), ) as DenoJson; async function replaceInFile( file: string, replacer: (content: string) => string, ) { const raw = await Deno.readTextFile(file); const replaced = replacer(raw); await Deno.writeTextFile(file, replaced); } const nextVersion = semver.format(next); function replaceJsonVersion(version: string) { return (content: string) => content.replace(/"version":\s"[^"]+"/, `"version": "${version}"`); } await replaceInFile(denoJsonPath, replaceJsonVersion(nextVersion)); await replaceInFile(updateJsonPath, replaceJsonVersion(nextVersion)); async function getNpmVersion(name: string) { const res = await fetch(`https://registry.npmjs.org/${name}`, { headers: { "Accept": "application/vnd.npm.install-v1+json; q=1.0, application/json; q=0.8, */*", }, }); const json = await res.json(); return json["dist-tags"].latest; } const [preactVersion, preactSignalsVersion] = await Promise.all([ getNpmVersion("preact"), getNpmVersion("@preact/signals"), ]); function updateVersions(content: string): string { const replaced = content .replace( /FRESH_VERSION\s=\s["']([^'"]+)['"]/g, `FRESH_VERSION = "${nextVersion}"`, ) .replace( /FRESH_TAILWIND_VERSION\s=\s["']([^'"]+)['"]/g, `FRESH_TAILWIND_VERSION = "${denoTailwindJson.version!}"`, ) .replace( /PREACT_VERSION\s=\s["']([^'"]+)['"]/g, `PREACT_VERSION = "${preactVersion!}"`, ) .replace( /PREACT_SIGNALS_VERSION\s=\s["']([^'"]+)['"]/g, `PREACT_SIGNALS_VERSION = "${preactSignalsVersion!}"`, ); if (content === replaced) { exitError(`Did not find FRESH_VERSION string`); } return replaced; } function replaceDepVersion( registry: "jsr" | "npm", name: string, version: string, ) { return (content: string) => { return content.replace( new RegExp(`"${name}":\\s"[^"]+"`), `"${name}": "${registry}:${name}@^${version}"`, ); }; } // Update preact + @preact/signals version await replaceInFile( denoJsonPath, replaceDepVersion("npm", "preact", preactVersion), ); await replaceInFile( denoJsonPath, replaceDepVersion("npm", "@preact/signals", preactSignalsVersion), ); await replaceInFile( wwwDenoJsonPath, replaceDepVersion("npm", "preact", preactVersion), ); await replaceInFile( wwwDenoJsonPath, replaceDepVersion("npm", "@preact/signals", preactSignalsVersion), ); const updateScriptPath = path.join( ROOT_DIR, "packages", "update", "src", "update.ts", ); await replaceInFile(updateScriptPath, updateVersions); const initScriptPath = path.join( ROOT_DIR, "packages", "init", "src", "init.ts", ); await replaceInFile(initScriptPath, updateVersions); ================================================ FILE: versions.json ================================================ [ "1.7.3", "1.7.2", "1.7.1", "1.7.0", "1.6.8", "1.6.7", "1.6.6", "1.6.5", "1.6.4", "1.6.3", "1.6.2", "1.6.1", "1.6.0", "1.5.4", "1.5.3", "1.5.2", "1.5.1", "1.5.0", "1.4.3", "1.4.2", "1.4.1", "1.4.0", "1.3.1", "1.3.0", "1.2.0", "1.1.6", "1.1.5", "1.1.4", "1.1.3", "1.1.2", "1.1.1", "1.1.0", "1.0.2", "1.0.1", "1.0.0", "1.0.0-rc.6", "1.0.0-rc.5", "1.0.0-rc.4", "1.0.0-rc.3", "1.0.0-rc.2", "1.0.0-rc.1" ] ================================================ FILE: www/README.md ================================================ # Fresh website This is the Fresh website source. The Fresh website contains: - a homepage - a documentation page ### Usage Start the project: ``` deno task start ``` This will watch the project directory and restart as necessary. ================================================ FILE: www/assets/styles.css ================================================ @import "tailwindcss"; @custom-variant dark (&:where([data-theme="dark"], [data-theme="dark"] *)); @theme { --color-fresh: hsl(50 100% 56%); --color-fresh-green: hsl(142 71% 29%); --color-background-primary: hsl(215deg 100% 100%); --color-background-secondary: hsl(210 29% 97%); --color-background-tertiary: hsl(207 33% 95%); --color-foreground-primary: hsl(0 0% 9%); --color-foreground-secondary: hsl(0 0% 23%); --color-foreground-tertiary: hsl(0 0% 32%); --color-foreground-quaternary: hsl(0 0% 42%); } @variant dark { --color-fresh: hsl(50 100% 56%); --color-fresh-green: hsl(142 71% 29%); --color-background-primary: hsl(220deg 11% 11%); --color-background-secondary: hsl(216deg 19% 18%); --color-background-tertiary: hsl(216deg 27.7% 22%); --color-foreground-primary: hsl(215deg 17% 99%); --color-foreground-secondary: hsl(215deg 17% 83%); --color-foreground-tertiary: hsl(215deg 17% 20%); --color-foreground-quaternary: hsl(215deg 17% 10%); --color-info: hsl(194 76% 41%); } html[data-theme="dark"]:root { color: var(--color-foreground-primary); background-color: var(--color-background-primary); } /* Scrollbar colors that look good on light and dark theme */ * { scrollbar-color: hsl(0deg 0% 50% / 0.5) hsl(0 0% 50% / 0.1) !important; } @font-face { font-family: Fixel; font-style: normal; src: url("/fonts/FixelVariable.woff2") format("woff2"); font-weight: 100 900; font-display: swap; } @font-face { font-family: Fixel; font-style: italic; src: url("/fonts/FixelVariableItalic.woff2") format("woff2"); font-weight: 100 900; font-display: swap; } body { font-family: Fixel, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif; font-weight: 450; font-size: 1.125rem; line-height: 1.5; color: #333; } h1, h2, h3, h4, h5, h6 { scroll-margin-top: 6rem; } hr { border-color: hsl(from var(--color-foreground-secondary) h s l / 0.1); } ::selection { background-color: #b1d5ff; } html[data-theme="dark"] ::selection { background-color: #064c9c; } .form-select-bg { background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='none'%3e%3cpath d='M7 7l3-3 3 3m0 6l-3 3-3-3' stroke='%239fa6b2' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3e%3c/svg%3e"); background-position: right 0.5rem center; background-size: 1.5em 1.5em; background-repeat: no-repeat; } .group[data-copied] .group-not-copied, .group:not([data-copied]) .group-copied { display: none; } ================================================ FILE: www/client.ts ================================================ import "./assets/styles.css"; document.addEventListener("click", async (ev) => { let el = ev.target as HTMLElement | null; if (el === null) return; if (!(el instanceof HTMLButtonElement)) { el = el.closest("button"); } if (el === null) return; const code = el.dataset.code; if (!code) return; try { await navigator.clipboard.writeText(code); el.dataset.copied = "true"; setTimeout(() => { delete el.dataset.copied; }, 1000); } catch (error) { const message = error instanceof Error ? error.message : String(error); // deno-lint-ignore no-console console.error(message || "Copy failed"); } }); ================================================ FILE: www/components/CodeBlock.tsx ================================================ import { Prism } from "../utils/prism.ts"; export function CodeBlock( { code, lang }: { code: string; lang: "js" | "ts" | "jsx" | "md" | "bash" }, ) { return (
); } ================================================ FILE: www/components/CodeWindow.tsx ================================================ import type { JSX } from "preact"; interface CodeWindowProps extends JSX.HTMLAttributes { name?: string; } export function CodeWindow(props: CodeWindowProps) { return (
*]:rounded-t-none ${ props.class ?? "" }`} style={props.style} >
{props.name ?? ""}
{props.children}
); } ================================================ FILE: www/components/CopyButton.tsx ================================================ import * as Icons from "./Icons.tsx"; export function CopyButton(props: { code: string }) { return (
); } ================================================ FILE: www/components/DocsSidebar.tsx ================================================ import type { TableOfContentsCategory, TableOfContentsCategoryEntry, } from "../data/docs.ts"; export function SidebarCategory(props: { category: TableOfContentsCategory; }) { const { title, href, entries } = props.category; return (
  • {title} {entries.length > 0 && (
      {entries.map((entry) => ( ))}
    )}
  • ); } export function SidebarEntry(props: { entry: TableOfContentsCategoryEntry; }) { const { title, href } = props.entry; return (
  • {title}
  • ); } ================================================ FILE: www/components/FancyLink.tsx ================================================ import type { ComponentChildren } from "preact"; export function FancyLink( props: { href: string; children: ComponentChildren; class?: string }, ) { return ( {props.children} ); } ================================================ FILE: www/components/FeatureIcons.tsx ================================================ export function NoBuild() { return ( ); } export function TypeScript() { return ( ); } export function Island() { return ( ); } // from https://heroicons.com/ export function Globe() { return ( ); } export function LightWeight() { return ( ); } export function Garbage() { return ( ); } ================================================ FILE: www/components/Footer.tsx ================================================ import type { JSX } from "preact"; const LINKS = [ { title: "Source", href: "https://github.com/denoland/fresh", }, { title: "License", href: "https://github.com/denoland/fresh/blob/main/LICENSE", }, { title: "Code of Conduct", href: "https://github.com/denoland/fresh/blob/main/CODE_OF_CONDUCT.md", }, ]; export default function Footer(props: JSX.HTMLAttributes) { return (
    © {new Date().getFullYear()} the Fresh authors
    {LINKS.map((link) => ( {link.title} ))}
    ); } ================================================ FILE: www/components/Header.tsx ================================================ import NavigationBar from "./NavigationBar.tsx"; export default function Header(props: { title: string; active: string }) { const isHome = props.active == "/"; const isDocs = props.active == "/docs"; const isShowcase = props.active == "/showcase"; return (
    {!isHome && (
    )}
    ); } export function Logo() { return ( Fresh logo ); } ================================================ FILE: www/components/Icons.tsx ================================================ export function IconMinus() { return ( ); } export function IconPlus() { return ( ); } export function Leaf() { return ( ); } export function Copy() { return ( ); } // from https://heroicons.com/ export function Check() { return ( ); } // from https://heroicons.com/ export function Info() { return ( ); } export function GitHub(props: { class?: string }) { return ( ); } export function Discord(props: { class?: string }) { return ( ); } export function ArrowRight() { return ( ); } ================================================ FILE: www/components/NavigationBar.tsx ================================================ import ThemeToggle from "../islands/ThemeToggle.tsx"; import * as Icons from "./Icons.tsx"; export default function NavigationBar( props: { active: string; class?: string }, ) { const items = [ { name: "Docs", href: "/docs", }, { name: "Showcase", href: "/showcase", }, { name: "Blog", href: "https://deno.com/blog?tag=fresh", }, ]; const isHome = props.active == "/"; const isDocs = props.active == "/docs"; return ( ); } ================================================ FILE: www/components/PageSection.tsx ================================================ import type { JSX } from "preact"; export function PageSection(props: JSX.HTMLAttributes) { return (
    {props.children}
    ); } ================================================ FILE: www/components/Projects.tsx ================================================ import * as Icons from "../components/Icons.tsx"; export interface Project { image: string; title: string; link: string; github?: string; } interface ProjectProps { items: Project[]; class?: string; } export default function Projects(props: ProjectProps) { return (
    {props.items.filter((item) => item.link.length > 0).map((project) => (
    {project.title}
    {project.github && ( GitHub )}
    ))}
    ); } ================================================ FILE: www/components/SideBySide.tsx ================================================ import type { JSX } from "preact/jsx-runtime"; type ColumnConfiguration = "1/1" | "2/3" | "3/2"; interface SideBySideProps extends JSX.HTMLAttributes { mdColSplit?: ColumnConfiguration; lgColSplit?: ColumnConfiguration; reverseOnDesktop?: boolean; } export function SideBySide(props: SideBySideProps) { let mdSplitClass = "md:grid-cols-2"; let lgSplitClass = "lg:grid-cols-2"; if (props.mdColSplit === "2/3") { mdSplitClass = "md:grid-cols-[minmax(0,2fr)_minmax(0,3fr)]"; } else if (props.mdColSplit === "3/2") { mdSplitClass = "md:grid-cols-[minmax(0,3fr)_minmax(0,2fr)]"; } if (props.lgColSplit === "2/3") { lgSplitClass = " lg:grid-cols-[minmax(0,2fr)_minmax(0,3fr)]"; } else if (props.lgColSplit === "3/2") { lgSplitClass = "lg:grid-cols-[minmax(0,3fr)_minmax(0,2fr)]"; } return (
    *]:md:first:order-1" : "" } ${mdSplitClass} ${lgSplitClass} ${props.class ?? ""}`} > {props.children}
    ); } ================================================ FILE: www/components/WaveTank.ts ================================================ export interface Spring { p: number; v: number; } export class WaveTank { springs = [] as Spring[]; waveLength = 100; k = 0.02; damping = 0.02; spread = 0.02; constructor() { for (let i = 0; i < this.waveLength; i++) { this.springs[i] = { p: 0, v: 0, }; } } update(springs: Spring[]) { for (const i of springs) { const a = -this.k * i.p - this.damping * i.v; i.p += i.v; i.v += a; } const leftDeltas = []; const rightDeltas = []; for (let t = 0; t < 8; t++) { for (let i = 0; i < springs.length; i++) { const prev = springs[(i - 1 + springs.length) % springs.length]; const next = springs[(i + 1) % springs.length]; leftDeltas[i] = this.spread * (springs[i].p - prev.p); rightDeltas[i] = this.spread * (springs[i].p - next.p); } for (let i = 0; i < springs.length; i++) { const prev = springs[(i - 1 + springs.length) % springs.length]; const next = springs[(i + 1) % springs.length]; prev.v += leftDeltas[i]; next.v += rightDeltas[i]; prev.p += leftDeltas[i]; next.p += rightDeltas[i]; } } } } ================================================ FILE: www/components/homepage/CTA.tsx ================================================ import { FancyLink } from "../FancyLink.tsx"; export function CTA() { return (
    Illustration of a lemon sliced cleanly in half, suspended in midair as though frozen in time the instant after the cut, the juice flung from the edges

    Time for a Fresh start

    Jump right in and build your website with Fresh. Learn everything you need to know in seconds.

    Get started
    ); } ================================================ FILE: www/components/homepage/CodeExampleBox.tsx ================================================ ================================================ FILE: www/components/homepage/DemoBox.tsx ================================================ import type { JSX } from "preact"; interface DemoBoxProps extends JSX.HTMLAttributes { flip?: boolean; } export function DemoBox(props: DemoBoxProps) { const outerFlip = props.flip ? "-skew-y-2 -skew-x-3" : "skew-y-2 skew-x-3"; const innerFlip = props.flip ? "skew-y-2 skew-x-3" : "-skew-y-2 -skew-x-3"; return (
    {props.children}
    ); } ================================================ FILE: www/components/homepage/DenoSection.tsx ================================================ import { PageSection } from "../PageSection.tsx"; import { FancyLink } from "../FancyLink.tsx"; import { SideBySide } from "../SideBySide.tsx"; import { SectionHeading } from "../homepage/SectionHeading.tsx"; export function DenoSection() { return ( Deno is drinking Fresh lemon squash
    Built on Deno

    Deno is the next evolution of server-side JavaScript, with stronger security, a robust built-in toolchain, and zero-config TypeScript support. (It's faster than Node, too.)

    Learn more about Deno
    ); } ================================================ FILE: www/components/homepage/ExampleArrow.tsx ================================================ import type { JSX } from "preact"; export function ExampleArrow( props: JSX.HTMLAttributes, ) { return ( ); } ================================================ FILE: www/components/homepage/FormsSection.tsx ================================================ import { PageSection } from "../PageSection.tsx"; import { SideBySide } from "../SideBySide.tsx"; import { CodeWindow } from "../CodeWindow.tsx"; import { CodeBlock } from "../CodeBlock.tsx"; import { SectionHeading } from "../homepage/SectionHeading.tsx"; import { DemoBox } from "../homepage/DemoBox.tsx"; import { ExampleArrow } from "../homepage/ExampleArrow.tsx"; import { FancyLink } from "../FancyLink.tsx"; import { FormSubmitDemo } from "../../islands/FormSubmitDemo.tsx"; const routingCode = `import { Handlers } from "$fresh/server.ts"; export const handler: Handlers = { async POST(req) { const form = await req.formData(); // Do something with the form data here, // then redirect user to thank you page const headers = new Headers(); headers.set("location", "/thanks"); return new Response(null, { status: 303, headers, }); }, };`; export function FormsSection() { return (
    Forms, the right way

    Don't fight the browser. Fresh helps you handle form submissions and other dynamic requests server-side, from any route.

    Since Fresh is built on{" "} Deno, it's built on web standards.

    Forms in Fresh
    ); } ================================================ FILE: www/components/homepage/Hero.tsx ================================================ import { FancyLink } from "../../components/FancyLink.tsx"; import LemonTop from "../../islands/LemonTop.tsx"; import LemonBottom from "../../islands/LemonBottom.tsx"; import { CopyButton } from "../CopyButton.tsx"; export function Hero() { return ( <>

    The simple, approachable, productive web framework

    Get started
    ); } function CopyArea(props: { code: string }) { return (
            {props.code}
          
    ); } ================================================ FILE: www/components/homepage/IslandsSection.tsx ================================================ import Counter from "../../islands/Counter.tsx"; import { CodeBlock } from "../../components/CodeBlock.tsx"; import { CodeWindow } from "../../components/CodeWindow.tsx"; import { PageSection } from "../../components/PageSection.tsx"; import { SideBySide } from "../../components/SideBySide.tsx"; import { SectionHeading } from "../../components/homepage/SectionHeading.tsx"; import { DemoBox } from "../../components/homepage/DemoBox.tsx"; import { ExampleArrow } from "../../components/homepage/ExampleArrow.tsx"; import { FancyLink } from "../../components/FancyLink.tsx"; const islandCode = `import { useSignal } from "@preact/signals"; export default function Counter(props) { const count = useSignal(props.start); return (

    Interactive island

    The server supplied the initial value of {props.start}.

    {count}
    ); }`; export function IslandsSection() { return (
    Island-based architecture

    Fresh ships plain HTML to the client, then hydrates with JavaScript only where needed.

    Because it's Preact, you get best-in-class performance, plus the convenience of{" "} Signals .

    Learn more about islands
    ); } ================================================ FILE: www/components/homepage/PartialsSection.tsx ================================================ import { CodeBlock } from "../../components/CodeBlock.tsx"; import { CodeWindow } from "../../components/CodeWindow.tsx"; import { PageSection } from "../../components/PageSection.tsx"; import { SideBySide } from "../../components/SideBySide.tsx"; import { SectionHeading } from "../../components/homepage/SectionHeading.tsx"; import { DemoBox } from "../../components/homepage/DemoBox.tsx"; import { ExampleArrow } from "../../components/homepage/ExampleArrow.tsx"; import { RecipeDemo } from "../../components/homepage/RecipeDemo.tsx"; import { FancyLink } from "../../components/FancyLink.tsx"; const islandCode = `import { Partial } from "$fresh/runtime.ts"; export const Recipes = () => (
    Click a recipe to stream HTML into this spot
    );`; export function PartialsSection() { return (
    Stream HTML straight from the server

    Fresh Partials let you fetch HTML and slot it directly into the page, without a full page reload—perfect for interactive elements and dynamic apps.

    Learn more about Partials
    ); } ================================================ FILE: www/components/homepage/RecipeDemo.tsx ================================================ import { Partial } from "fresh/runtime"; export const RecipeDemo = () => (
    Click a recipe to stream HTML into this spot
    ); ================================================ FILE: www/components/homepage/RenderingSection.tsx ================================================ import { PageSection } from "../../components/PageSection.tsx"; import { SideBySide } from "../../components/SideBySide.tsx"; import { CodeWindow } from "../../components/CodeWindow.tsx"; import { CodeBlock } from "../../components/CodeBlock.tsx"; import { SectionHeading } from "../../components/homepage/SectionHeading.tsx"; import { DemoBox } from "../../components/homepage/DemoBox.tsx"; import { ExampleArrow } from "../../components/homepage/ExampleArrow.tsx"; const serverCode = `export default function HomePage() { const time = new Date().toLocaleString(); return (

    Freshly server-rendered {time}

    ); }`; export function RenderingSection() { return (
    Build fast apps fast

    Fresh routes are dynamically server-rendered{" "} Preact{" "} components, so there's zero JavaScript shipped to the browser by default.

    Simple to write; fast to run.

    {" "}

    Freshly server-rendered {new Date().toLocaleString("default", { dateStyle: "medium", timeStyle: "medium", })} UTC

    ); } ================================================ FILE: www/components/homepage/SectionHeading.tsx ================================================ import type { ComponentChildren } from "preact"; export function SectionHeading( { children }: { children: ComponentChildren }, ) { return (

    {children}

    ); } ================================================ FILE: www/components/homepage/Simple.tsx ================================================ import { PageSection } from "../../components/PageSection.tsx"; export function Simple() { return (

    Introducing Fresh:

    The framework so simple, you already know it.

    Fresh is designed to be easy to use by building on the best well-known tools, conventions, and web standards.

    ); } ================================================ FILE: www/components/homepage/SocialProof.tsx ================================================ import { PageSection } from "../../components/PageSection.tsx"; import { FancyLink } from "../../components/FancyLink.tsx"; import { DemoBox } from "../../components/homepage/DemoBox.tsx"; export function SocialProof() { return (

    Built for the edge

    Fresh is the secret sauce behind production-grade, enterprise-ready software like{" "} Deco.cx, Brazil's top eCommerce platform

    Deco CX
    The team also used{" "} Fresh, a next-gen Deno-native full stack web framework that sends zero JavaScript to the client, for its modern developer experience and snappy performance…

    This stack unlocked{" "} 5x faster page load speeds and a 30% jump in conversion rates {" "} for their clients.
    Read the case study
    ); } ================================================ FILE: www/data/docs.ts ================================================ import toc from "../../docs/toc.ts"; export interface TableOfContentsEntry { slug: string; title: string; category?: string; href: string; file: string; } export interface TableOfContentsCategory { title: string; href: string; entries: TableOfContentsCategoryEntry[]; } export interface TableOfContentsCategoryEntry { title: string; href: string; } export const TABLE_OF_CONTENTS: Record< string, Record > = {}; export const CATEGORIES: Record = {}; export const VERSIONS = Object.keys(toc); export const CANARY_VERSION = toc.canary ? "canary" : ""; export const LATEST_VERSION = VERSIONS.find((version) => version !== "canary") ?? ""; for (const version in toc) { const RAW_VERSION = toc[version]; const versionSlug = version === LATEST_VERSION ? "" : `/${version}`; TABLE_OF_CONTENTS[version] = {}; CATEGORIES[version] = []; for (const parent in RAW_VERSION.content) { const rawEntry = RAW_VERSION.content[parent]; // Allow versioned documentation to stack on each other. This should // only be used for canary versions. This avoids having us to copy // all documentation content and backport changes. const fileVersion = rawEntry.link ?? version; const versionFilePath = fileVersion === LATEST_VERSION ? "/latest" : `/${fileVersion}`; const href = `/docs${versionSlug}/${parent}`; const file = `docs${versionFilePath}/${parent}/index.md`; const entry = { slug: parent, title: rawEntry.title, href, file, }; TABLE_OF_CONTENTS[version][parent] = entry; const category: TableOfContentsCategory = { title: rawEntry.title, href, entries: [], }; CATEGORIES[version].push(category); if (rawEntry.pages) { for (const [id, title, linkedVersion] of rawEntry.pages) { const slug = `${parent}/${id}`; // Allow stacked documentation const pageVersion = linkedVersion ? linkedVersion.slice("link:".length) : version; const versionFilePath = !pageVersion || pageVersion === LATEST_VERSION ? "/latest" : `/${pageVersion}`; const href = `/docs${versionSlug}/${slug}`; const file = `docs${versionFilePath}/${slug}.md`; const entry = { slug, title, category: parent, href, file }; TABLE_OF_CONTENTS[version][slug] = entry; category.entries.push({ title, href, }); } } } } export function getFirstPageUrl(version: string) { const group = TABLE_OF_CONTENTS[version]; if (group) { for (const slug in group) { return group[slug].href; } } throw new Error(`Could not find version "${version}"`); } ================================================ FILE: www/data/showcase.json ================================================ [ { "title": "JSR", "link": "https://jsr.io/", "github": "jsr-io/jsr", "image": "jsr" }, { "title": "Deno.com", "link": "https://deno.com/", "image": "deno" }, { "title": "OpenAI Semantic Search", "link": "https://supabase-openai-doc-search.deno.dev/", "github": "supabase-community/deno-fresh-openai-doc-search", "image": "openai-semantic-search" }, { "title": "Deno SaaSKit", "link": "https://saaskit.deno.dev/", "github": "denoland/saaskit", "image": "deno-saaskit" }, { "title": "Shoaib Hossain Portfolio", "link": "https://sihab.deno.dev", "github": "mrsihab/fresh-portfolio", "image": "mrsihab" }, { "title": "Deno Merch", "link": "https://merch.deno.com/", "github": "denoland/merch", "image": "merch" }, { "title": "Fresh Website", "link": "https://fresh.deno.dev/", "github": "denoland/fresh/tree/main/www", "image": "fresh" }, { "title": "Video Poker Academy", "link": "https://videopoker-academy.deno.dev/", "github": "hyprtxt/videopoker.academy", "image": "videopoker-academy" }, { "title": "D3no Data", "link": "https://d3nodata.deno.dev/", "github": "D3nosaurs/d3nodata-website", "image": "d3nodata" }, { "title": "Fresh Hacker News", "link": "https://fresh-hacker-news.deno.dev/", "github": "morinokami/fresh-hacker", "image": "hn" }, { "title": "LDkit", "link": "https://ldkit.io", "github": "karelklima/ldkit", "image": "ldkit" }, { "title": "CGPA(AU R-2021) Calculator", "link": "https://cgpa-au-2021.deno.dev", "github": "codesculpture/cgpa-au-2021", "image": "cgpa-au-2021" }, { "title": "diKnow", "link": "https://xyntechx-diknow.deno.dev/", "github": "xyntechx/diKnow", "image": "diKnow" }, { "title": "Fresh Strapi", "link": "https://fresh-strapi.deno.dev/", "github": "hyprtxt/fresh-strapi.deno.dev", "image": "fresh-strapi" }, { "title": "Portfolio Page", "link": "http://adamsobotka.deno.dev/", "github": "vorcigernix/adamsobotka", "image": "adam-portfolio" }, { "title": "Developer Portfolio", "link": "https://mooxl.dev", "github": "mooxl/fresh.mooxl.dev", "image": "mooxl" }, { "title": "Developer Portfolio", "link": "https://guillaumecomte.deno.dev", "github": "guigui64/www", "image": "guigui64" }, { "title": "Fresh Project Manager", "link": "https://xyntechx-project-manager.deno.dev/", "github": "xyntechx/NexLiber-Projects/tree/main/Fresh%20Project%20Manager", "image": "fresh-project-manager" }, { "title": "Fresh ToDo List App", "link": "https://fresh-todo-list.deno.dev/", "github": "MrSaeedNasiri/fresh-todo-list", "image": "fresh-todo" }, { "title": "Optimem app", "link": "https://optimem.org/", "image": "optimem" }, { "title": "Balello", "link": "https://balello.com", "image": "balello" }, { "title": "Wricord", "link": "https://wricord.com", "image": "wricord" }, { "title": "ANDBOUNDS Inc.", "link": "https://andbounds.com/", "image": "andbounds" }, { "title": "YCRM", "link": "https://ycrm.xyz/", "image": "ycrm" }, { "title": "Local SEO Studio", "link": "https://www.localseo.studio/", "image": "localseostudio" }, { "title": "Fondest Landing Page", "link": "https://fondest.com", "image": "fondest" }, { "title": "im@sparql Data Dashboard", "link": "https://imasparql-data-dashboard.deno.dev/", "github": "arrow2nd/imasparql-data-dashboard", "image": "imasparql-dd" }, { "title": "Craig’s Deno Diary", "link": "https://deno-blog.com", "github": "cdoremus/deno-blog", "image": "deno-diary" }, { "title": "Melon", "link": "https://m.not-ivy.dev/", "github": "not-ivy/melon", "image": "melon" }, { "title": "Webhook Manager", "link": "https://webhook-demo.deno.dev/", "github": "avnigenc/deno-fresh-webhook-demo", "image": "webhook-manager" }, { "title": "Deno Jobs", "link": "https://vacancy.deno.dev/", "github": "VofSwords/deno-jobs", "image": "deno-jobs" }, { "title": "java.minidoodle", "link": "https://javaminidoodle.de/", "image": "javaminidoodle" }, { "title": "Ppaste", "link": "https://ppaste.deno.dev/", "github": "n4ze3m/p-paste", "image": "ppaste" }, { "title": "Linksapp", "link": "https://linksapp.deno.dev/", "github": "commune-org/linksapp-fresh", "image": "linksapp" }, { "title": "Grape Chat", "link": "https://grape-chat.miqsel.net/", "github": "yahiro07/grape-chat", "image": "grape-chat" }, { "title": "Fresh Notion Blog", "link": "https://fresh-notion-blog.deno.dev/", "github": "xijaja/fresh-notion-blog", "image": "fresh-notion-blog" }, { "title": "Denopaste", "link": "https://denopaste.com/", "github": "stephenmelnicki/denopaste", "image": "denopaste" }, { "title": "m33t", "link": "https://m33t.deno.dev/", "github": "dunkbing/m33t", "image": "m33t" }, { "title": "YAML to TypeScript", "link": "https://yaml-to-ts.deno.dev/", "github": "jiawei397/yaml_to_ts_web", "image": "yaml-to-ts" }, { "title": "Denosoar", "link": "https://denosoar.deno.dev/", "github": "Denosoar-GUI/Denosoar-GUI", "image": "Denosoar" }, { "title": "link-maker", "link": "https://link-maker.deno.dev/", "github": "kuizuo/link-maker", "image": "link-maker" }, { "title": "Delicious", "link": "https://sking.deno.dev/", "github": "shijianzhong/fresh-blog-system", "image": "fresh-blog-system" }, { "title": "Alvaro Vanegas Sites", "link": "https://ajvanegasv.dev/", "github": "ajvanegasv/my-portfolio", "image": "ajvanegasv" }, { "title": "Teams Template", "link": "https://teams-template.deno.dev/", "github": "davit-b/teams-template", "image": "teams-template" }, { "title": "FestWithMe", "link": "https://fest-with-me.deno.dev/", "github": "rnaidenov/fest-with-me", "image": "festwithme" }, { "title": "Pollify", "link": "https://pollify.deno.dev/", "github": "n4ze3m/pollify", "image": "pollify" }, { "title": "JPT ChatGPT Rooms", "link": "https://jpt.ma/", "github": "zizwar/jpt", "image": "jptchat" }, { "title": "Mieszko", "link": "https://mieszko.xyz/", "image": "mieszko" }, { "title": "Awesome Fresh", "link": "https://uki00a.github.io/awesome-fresh/", "image": "awesome-fresh" }, { "title": "Learn Mandarin", "link": "https://mandarin.deno.dev/", "github": "silvercrow/mandarin", "image": "learn-mandarin" }, { "title": "Url shorter and Pastebin", "link": "https://uspb.deno.dev/", "github": "jneeee/uspb", "image": "uspb" }, { "title": "Battleship", "link": "https://battleship.deno.dev", "github": "karelklima/battleship", "image": "battleship" }, { "title": "Kanji.Academy", "link": "https://kanji.academy", "image": "kanji-academy" }, { "title": "LiberChat", "link": "https://liberchat.deno.dev", "image": "liberchat" }, { "title": "Miguel Rangel Site", "link": "https://crawford.ml", "github": "denyncrawford/new-portfolio", "image": "miguel-rangel-site" }, { "title": "Deno Artwork", "link": "https://artwork.deno.dev/", "github": "jasonjgardner/deno-artwork", "image": "deno-artwork" }, { "title": "Text2Audio", "link": "https://text2audio.cc/", "github": "dunkbing/text2audio", "image": "text2audio" }, { "title": "FilterHN", "link": "https://filterhn.com/", "image": "filter-hn" }, { "title": "AquaVibes", "link": "https://aquavibes.earth/", "github": "arturolume/aquavibes", "image": "aquavibes" }, { "title": "CS2 Items", "link": "https://cs2items.pro/", "image": "cs2items" }, { "title": "Enric Perpinyà Site", "link": "https://perpinya.eu/", "github": "evilmonkey19/cv", "image": "evilmonkey19-cv" }, { "title": "Paul Jacks Blog and Experiments", "link": "https://jacks.se/", "github": "rebstorm/jacks.se", "image": "rebstorm" }, { "title": "MergePanic", "link": "https://mergepanic.com", "image": "MergePanic" }, { "title": "Living Pixel Solutions", "link": "https://livingpixel.io", "image": "living-pixel" }, { "title": "RFUI", "link": "https://rfui.deno.dev/", "image": "rfui" }, { "title": "Echo-Echo", "link": "https://echo-echo.deno.dev/", "github": "Octo8080X/Echo-Echo", "image": "echo-echo" }, { "title": "VirtualFreight Landing Page", "link": "https://virtualfreight.software/", "image": "virtualfreight" }, { "title": "Daniel G. Aubert's Site", "link": "https://dgaubert.dev/", "image": "dgaubert" }, { "title": "Do not remove this, it’s for preventing conflicts by trailing comma", "link": "", "github": "", "image": "" }, { "title": "Thoth-Doc", "link": "https://thoth-doc.deno.dev/", "github": "Octo8080X/thoth", "image": "thoth-doc" }, { "title": "Pile-Up", "link": "https://pile-up.deno.dev/", "github": "Octo8080X/Pile-Up", "image": "pile-up" }, { "title": "2coffee's Blog", "link": "https://2coffee.dev/", "image": "hicoffee" }, { "title": "TicOXs", "link": "https://ticoxs.deno.dev/", "github": "Trid3r/ticoxs", "image": "ticoxs" }, { "title": "WTF Is Congress Doing Now?", "link": "https://wtfiscongressdoingnow.us", "github": "gibbygano/wtfiscongressdoingnow", "image": "wtfcongress" }, { "title": "PUBG Items", "link": "https://pubgitems.info", "image": "pubgitems" }, { "title": "Zhi's Blog", "link": "https://zhi.deno.dev", "github": "SisyphusZheng/me", "image": "zhiblog" }, { "title": "Moatez's desktop", "link": "https://moatez-bejaoui.deno.dev", "github": "https://github.com/MoatezNG", "image": "moatez-desktop" }, { "title": "Retro Ranker", "link": "https://retroranker.site", "github": "https://github.com/Nergy101/retro-ranker", "image": "retro-ranker" } ] ================================================ FILE: www/deno.json ================================================ { "tasks": { "start": "deno serve -A _fresh/server.js", "dev": "vite", "build": "vite build" }, "imports": { "@tailwindcss/vite": "npm:@tailwindcss/vite@^4.1.12", "vite": "npm:vite@^7.1.4", "vite-plugin-inspect": "npm:vite-plugin-inspect@^11.3.2" } } ================================================ FILE: www/dev.ts ================================================ #!/usr/bin/env -S deno run -A --watch=static/,routes/ import { Builder } from "fresh/dev"; import { tailwind } from "@fresh/plugin-tailwind"; const builder = new Builder({ target: "safari12" }); tailwind(builder); if (Deno.args.includes("build")) { await builder.build(); } else { await builder.listen(() => import("./main.ts")); } ================================================ FILE: www/islands/Counter.tsx ================================================ import { useSignal } from "@preact/signals"; import * as Icons from "../components/Icons.tsx"; import type { JSX } from "preact"; interface CounterProps { start: number; } export default function Counter(props: CounterProps) { const count = useSignal(props.start); return (

    Interactive island

    The server supplied the initial value of 3.

    count.value -= 1} >
    {count}
    count.value += 1} >
    ); } function RoundedButton(props: JSX.HTMLAttributes) { return ( ); } ================================================ FILE: www/islands/LemonBottom.tsx ================================================ import { useEffect, useRef } from "preact/hooks"; import { useSignal } from "@preact/signals"; import { WaveTank } from "../components/WaveTank.ts"; function easeInCirc(x: number) { return 1 - Math.sqrt(1 - Math.pow(x, 2)); } const waveTank = new WaveTank(); function LemonBottom() { const SVG_WIDTH = 100; const counter = useSignal(0); const dropY = useSignal(60); const width = useSignal(SVG_WIDTH); const widthRef = useRef(width.value); const springs = useSignal(waveTank.springs); const requestIdRef = useRef(); const grid = SVG_WIDTH / waveTank.waveLength; let lemonTop: SVGElement | null; let lemonTopLeft: number; const points = [ [0, 100], [0, 0], ...springs.value.map((x, i) => [i * grid, x.p]), [width.value, 0], [width.value, 100], ]; const springsPath = `${points.map((x) => x.join(",")).join(" ")}`; function updateJuice(timestamp: number) { const amp = 40; const x = timestamp / 2000; const saw = x - Math.floor(x); if (saw < 0.6) { counter.value = easeInCirc(saw) * amp; dropY.value = -100; } else { counter.value = easeInCirc(1 - saw) * amp * 0.1; dropY.value = 70 + Math.pow(saw - 0.6, 2) * 10000; } } function update(timestamp: number) { updateJuice(timestamp); waveTank.update(waveTank.springs); springs.value = [...waveTank.springs]; const offset = 500; const saw = (timestamp + offset) / 2000 - Math.floor((timestamp + offset) / 2000); if (saw < 0.01) { drop(); } requestIdRef.current = globalThis.requestAnimationFrame(update); } function resize() { width.value = document.body.clientWidth; lemonTop = document.querySelector("#lemon-top"); lemonTopLeft = lemonTop ? lemonTop.getBoundingClientRect().left + 25 : 50; } function drop() { let dropPosition = 50; if (widthRef.current >= 768) { dropPosition = Math.round(100 / widthRef.current * lemonTopLeft); } else { dropPosition = Math.round( ((widthRef.current / 2 - 30) / widthRef.current) * 100, ); } waveTank.springs[dropPosition].p = -40; } useEffect(() => { widthRef.current = width.value; }, [width.value]); useEffect(() => { const mediaQuery = globalThis.matchMedia( "(prefers-reduced-motion: reduce)", ); if (mediaQuery.matches) { return; } requestIdRef.current = requestAnimationFrame(update); globalThis.addEventListener("resize", resize); resize(); return () => { globalThis.removeEventListener("resize", resize); if (requestIdRef.current !== undefined) { cancelAnimationFrame(requestIdRef.current); } }; }, []); return ( ); } export default LemonBottom; ================================================ FILE: www/islands/LemonDrop.tsx ================================================ import { useEffect, useRef } from "preact/hooks"; import { useSignal } from "@preact/signals"; import { WaveTank } from "../components/WaveTank.ts"; function easeInCirc(x: number) { return 1 - Math.sqrt(1 - Math.pow(x, 2)); } const waveTank = new WaveTank(); function LemonDrop() { const SVG_WIDTH = 100; const counter = useSignal(0); const dropY = useSignal(60); const width = useSignal(SVG_WIDTH); const widthRef = useRef(width.value); const springs = useSignal(waveTank.springs); const requestIdRef = useRef(); const grid = SVG_WIDTH / waveTank.waveLength; const points = [ [0, 100], [0, 0], ...springs.value.map((x, i) => [i * grid, x.p]), [width.value, 0], [width.value, 100], ]; const springsPath = `${points.map((x) => x.join(",")).join(" ")}`; const juice = `M18 ${63 + counter.value} C15 ${63 + counter.value} 16 ${ 63 + counter.value } 12 61L9 56C2 33 62 -3 80 12C103 27 44 56 29 58C27 58 25 59 24 61C20 ${ 63 + counter.value } 21 ${63 + counter.value} 18 ${63 + counter.value}Z`; function updateJuice(timestamp: number) { const amp = 40; const x = timestamp / 2000; const saw = x - Math.floor(x); if (saw < 0.6) { counter.value = easeInCirc(saw) * amp; dropY.value = -100; } else { counter.value = easeInCirc(1 - saw) * amp * 0.1; dropY.value = 70 + Math.pow(saw - 0.6, 2) * 10000; } } function update(timestamp: number) { updateJuice(timestamp); waveTank.update(waveTank.springs); springs.value = [...waveTank.springs]; const offset = 500; const saw = (timestamp + offset) / 2000 - Math.floor((timestamp + offset) / 2000); if (saw < 0.01) { drop(); } requestIdRef.current = globalThis.requestAnimationFrame(update); } function resize() { width.value = document.body.clientWidth; } function drop() { const dropPosition = Math.round( ((widthRef.current / 2 - 30) / widthRef.current) * 100, ); waveTank.springs[dropPosition].p = -40; } useEffect(() => { widthRef.current = width.value; }, [width.value]); useEffect(() => { const mediaQuery = globalThis.matchMedia( "(prefers-reduced-motion: reduce)", ); if (mediaQuery.matches) { return; } requestIdRef.current = requestAnimationFrame(update); globalThis.addEventListener("resize", resize); resize(); return () => { globalThis.removeEventListener("resize", resize); if (requestIdRef.current !== undefined) { cancelAnimationFrame(requestIdRef.current); } }; }, []); return ( <> Fresh logo ); } export default LemonDrop; ================================================ FILE: www/islands/LemonTop.tsx ================================================ import { useEffect, useRef } from "preact/hooks"; import { useSignal } from "@preact/signals"; import { WaveTank } from "../components/WaveTank.ts"; function easeInCirc(x: number) { return 1 - Math.sqrt(1 - Math.pow(x, 2)); } const waveTank = new WaveTank(); function LemonTop() { const SVG_WIDTH = 100; const counter = useSignal(0); const dropY = useSignal(60); const width = useSignal(SVG_WIDTH); const widthRef = useRef(width.value); const springs = useSignal(waveTank.springs); const requestIdRef = useRef(); const juice = `M18 ${63 + counter.value} C15 ${63 + counter.value} 16 ${ 63 + counter.value } 12 61L9 56C2 33 62 -3 80 12C103 27 44 56 29 58C27 58 25 59 24 61C20 ${ 63 + counter.value } 21 ${63 + counter.value} 18 ${63 + counter.value}Z`; function updateJuice(timestamp: number) { const amp = 40; const x = timestamp / 2000; const saw = x - Math.floor(x); if (saw < 0.6) { counter.value = easeInCirc(saw) * amp; dropY.value = -100; } else { counter.value = easeInCirc(1 - saw) * amp * 0.1; dropY.value = 70 + Math.pow(saw - 0.6, 2) * 10000; } } function update(timestamp: number) { updateJuice(timestamp); waveTank.update(waveTank.springs); springs.value = [...waveTank.springs]; const offset = 500; const saw = (timestamp + offset) / 2000 - Math.floor((timestamp + offset) / 2000); if (saw < 0.01) { drop(); } requestIdRef.current = globalThis.requestAnimationFrame(update); } function resize() { width.value = document.body.clientWidth; } function drop() { const dropPosition = Math.round( ((widthRef.current / 2 - 30) / widthRef.current) * 100, ); waveTank.springs[dropPosition].p = -40; } useEffect(() => { widthRef.current = width.value; }, [width.value]); useEffect(() => { const mediaQuery = globalThis.matchMedia( "(prefers-reduced-motion: reduce)", ); if (mediaQuery.matches) { return; } requestIdRef.current = requestAnimationFrame(update); globalThis.addEventListener("resize", resize); resize(); return () => { globalThis.removeEventListener("resize", resize); if (requestIdRef.current !== undefined) { cancelAnimationFrame(requestIdRef.current); } }; }, []); return ( <> ); } export default LemonTop; ================================================ FILE: www/islands/SearchButton.tsx ================================================ import { useEffect, useRef } from "preact/hooks"; import docsearch from "docsearch"; // Copied from algolia source code type DocSearchProps = { appId: string; apiKey: string; indexName: string; container: HTMLElement | string; }; export default function SearchButton( props: { docsearch?: (args: DocSearchProps) => void; class?: string }, ) { const ref = useRef(null); useEffect(() => { if (ref.current) { props.docsearch || docsearch({ appId: "CWUS37S0PK", apiKey: "caa591b6dcb2c9308551361d954a728b", indexName: "fresh", container: ref.current, }); } }, [ref.current]); return (
    ); } ================================================ FILE: www/islands/TableOfContents.tsx ================================================ import { useEffect, useRef } from "preact/hooks"; import type { MarkdownHeading } from "../utils/markdown.ts"; import { useSignal } from "@preact/signals"; export interface TableOfContentsProps { headings: MarkdownHeading[]; } function setActiveLink( container: HTMLElement, marker: HTMLElement, id: string, ) { container.querySelectorAll(`a`).forEach((link) => link.classList.remove("active") ); const tocLink = container.querySelector( `a[href="#${id}"]`, ) as HTMLElement; if (tocLink === null) return; tocLink.classList.add("active"); const rect = tocLink .getBoundingClientRect(); const markerRect = marker.getBoundingClientRect(); const top = tocLink.offsetTop + (rect.height / 2) - (markerRect.height / 2); marker.style.cssText = `transform: translate3d(0, ${top}px, 0); opacity: 1`; } function hLevelToClass(level: number): string { if (level === 3) return "ml-4"; if (level === 4) return "ml-8"; return ""; } export function TableOfContents({ headings }: TableOfContentsProps) { const ref = useRef(null); const refMarker = useRef(null); const isOpen = useSignal(false); useEffect(() => { if (!ref.current) return; const container = ref.current; const activeList = new Array(headings.length).fill(false); const visibleList = new Array(headings.length).fill(false); const marker = refMarker.current!; const observer = new IntersectionObserver((entries) => { for (let i = 0; i < entries.length; i++) { const entry = entries[i]; const target = entry.target; for (let j = 0; j < headings.length; j++) { const heading = headings[j]; if (heading.id === target.id) { const active = entry.isIntersecting || entry.boundingClientRect.top < 0; activeList[j] = active; visibleList[j] = entry.isIntersecting; } } } // Reset links for (let i = 0; i < headings.length; i++) { const id = headings[i].id; const tocLink = container.querySelector( `a[href="#${id}"]`, ); if (tocLink !== null) { tocLink.classList.remove("active"); } } let activeIdx = visibleList.indexOf(true); if (activeIdx < 0) { activeIdx = activeList.lastIndexOf(true); } if (activeIdx > -1) { const id = headings[activeIdx].id; setActiveLink(container, marker, id); } else { marker.style.cssText = `transform: translate3d(0, 0, 0); opacity: 0`; } }); document.querySelectorAll( ".markdown-body h2, .markdown-body h3, .markdown-body h4, .markdown-body h5, .markdown-body h6", ).forEach((elem) => { observer.observe(elem); }); return () => { observer.disconnect(); }; }, [headings]); return (
    {headings.length > 0 && ( <>
    {isOpen.value && (
    )}
    ); } ================================================ FILE: www/islands/ThemeToggle.tsx ================================================ import { IS_BROWSER } from "fresh/runtime"; import { useSignal } from "@preact/signals"; export default function ThemeToggle() { const theme = useSignal( !IS_BROWSER ? "light" : document.documentElement.dataset.theme ?? "light", ); const toggleTheme = () => { theme.value = theme.value === "light" ? "dark" : "light"; document.documentElement.setAttribute("data-theme", theme.value); localStorage.setItem("theme", theme.value); }; return ( ); } ================================================ FILE: www/islands/VersionSelect.tsx ================================================ // Copyright 2022-2023 the Deno authors. All rights reserved. MIT license. import { IS_BROWSER } from "fresh/runtime"; import type { VersionLink } from "../routes/docs/[...slug].tsx"; export default function VersionSelect( { versions, selectedVersion }: { versions: VersionLink[]; selectedVersion: string; }, ) { const selectedIsLatest = selectedVersion === "latest"; const selectedIsCanary = selectedVersion === "canary"; return ( <>
    {selectedIsLatest && (
    Latest
    )} {selectedIsCanary && (
    🚧 Preview
    )}
    ); } ================================================ FILE: www/main.ts ================================================ import { App, staticFiles, trailingSlashes } from "fresh"; export const app = new App() .use(staticFiles()) .use(trailingSlashes("never")) .fsRoutes(); ================================================ FILE: www/main_test.ts ================================================ import { withBrowser, withChildProcessServer, } from "../packages/fresh/tests/test_utils.tsx"; import { expect } from "@std/expect"; import { retry } from "@std/async/retry"; import { buildVite, launchProd, } from "../packages/plugin-vite/tests/test_utils.ts"; let result: Awaited>; Deno.test.beforeAll(async () => { result = await buildVite(import.meta.dirname!); }); Deno.test.afterAll(async () => { await result?.[Symbol.asyncDispose](); }); Deno.test({ name: "CORS should not set on GET /fresh-badge.svg", sanitizeOps: false, sanitizeResources: false, fn: async () => { await launchProd({ cwd: result.tmp }, async (address) => { const resp = await fetch(`${address}/fresh-badge.svg`); await resp?.body?.cancel(); expect(resp.headers.get("cross-origin-resource-policy")).toEqual(null); }); }, }); Deno.test({ name: "shows version selector", sanitizeOps: false, sanitizeResources: false, fn: async () => { await retry(async () => { await withChildProcessServer( { cwd: result.tmp, args: ["serve", "-A", "--port", "0", "_fresh/server.js"], }, async (address) => { await withBrowser(async (page) => { await page.goto(`${address}/docs`); await page.waitForSelector("#version"); // Check that we redirected to the first page await page.waitForFunction(() => { const url = new URL(window.location.href); return url.pathname === "/docs/introduction"; }); // Wait for version selector to be enabled await page.waitForSelector("#version:not([disabled])"); const options = await page .locator("#version") .evaluate((el) => { return Array.from(el.options).map((option) => ({ value: option.value, label: option.textContent, })); }); expect(options.map((item) => item.value)).toEqual([ "latest", "1.x", ]); const selectValue = await page .locator("#version") .evaluate((el) => el.value); expect(selectValue).toEqual("latest"); // Go to canary page await page.evaluate(() => { const el = document.querySelector( "#version", ) as HTMLSelectElement; el.value = "1.x"; el.dispatchEvent(new Event("change")); }); await new Promise((r) => setTimeout(r, 1000)); await page.waitForSelector("#version:not([disabled])"); const selectValue2 = await page .locator("#version") .evaluate((el) => el.value); expect(selectValue2).toEqual("1.x"); await page.waitForFunction(() => { const url = new URL(window.location.href); return url.pathname === "/docs/1.x/introduction"; }); }); }, ); }); }, }); ================================================ FILE: www/routes/_app.tsx ================================================ import { asset } from "fresh/runtime"; import { define } from "../utils/state.ts"; export default define.page(function App({ Component, state, url }) { return ( {state.title ? {state.title} : null} {state.description ? : null} {state.title ? : null} {state.description ? : null} {state.ogImage ? : null} {state.noIndex ? : null} {url.pathname === "/" ? : null} {url.pathname.startsWith("/docs/") ? ( <> ) : null} ); }); ================================================ FILE: www/routes/_error.tsx ================================================ import { HttpError, type PageProps } from "fresh"; import LemonDrop from "../islands/LemonDrop.tsx"; export function ServerCodePage( props: { serverCode: number; codeDescription: string }, ) { return ( <>
    ); } export default function ErrorPage(props: PageProps) { const error = props.error; if (error instanceof HttpError) { if (error.status === 404) { return ServerCodePage({ serverCode: 404, codeDescription: "Couldn’t find what you’re looking for.", }); } } // deno-lint-ignore no-console console.error(error); return ServerCodePage({ serverCode: 500, codeDescription: "Oops! Something went wrong.", }); } ================================================ FILE: www/routes/_middleware.ts ================================================ import type { Context } from "fresh"; import type { Event } from "$ga4"; import { GA4Report, isDocument, isServerError } from "$ga4"; const GA4_MEASUREMENT_ID = Deno.env.get("GA4_MEASUREMENT_ID"); let showedMissingEnvWarning = false; function ga4( request: Request, conn: Context, response: Response, _start: number, error?: unknown, ) { if (GA4_MEASUREMENT_ID === undefined) { if (!showedMissingEnvWarning) { showedMissingEnvWarning = true; // deno-lint-ignore no-console console.warn( "GA4_MEASUREMENT_ID environment variable not set. Google Analytics reporting disabled.", ); } return; } Promise.resolve().then(async () => { // We're tracking page views and file downloads. These are the only two // HTTP methods that _might_ be used. if (!/^(GET|POST)$/.test(request.method)) { return; } // If the visitor is using a web browser, only create events when we serve // a top level documents or download; skip assets like css, images, fonts. if (!isDocument(request, response) && error == null) { return; } let event: Event | null = null; const contentType = response.headers.get("content-type"); if (/text\/html/.test(contentType!)) { event = { name: "page_view", params: {} }; // Probably an old browser. } if (event == null && error == null) { return; } // If an exception was thrown, build a separate event to report it. let exceptionEvent; if (error != null) { exceptionEvent = { name: "exception", params: { description: String(error), fatal: isServerError(response), }, }; } else { exceptionEvent = undefined; } // Create basic report. const measurementId = GA4_MEASUREMENT_ID; // @ts-ignore GA4Report doesn't even use the localAddress parameter const report = new GA4Report({ measurementId, request, response, // Doesn't use localAddr // deno-lint-ignore no-explicit-any conn: conn.info as any, }); // Override the default (page_view) event. report.event = event; // Add the exception event, if any. if (exceptionEvent != null) { report.events.push(exceptionEvent); } await report.send(); }).catch((err) => { // deno-lint-ignore no-console console.error(err); }); } export async function handler( ctx: Context, ): Promise { let err; let res: Response; const start = performance.now(); try { const resp = await ctx.next(); const headers = new Headers(resp.headers); res = new Response(resp.body, { status: resp.status, headers }); return res; } catch (e) { res = new Response("Internal Server Error", { status: 500, }); err = e; throw e; } finally { ga4( ctx.req, ctx, res!, start, err, ); } } ================================================ FILE: www/routes/docs/[...slug].tsx ================================================ import { HttpError, page } from "fresh"; import { asset, Partial } from "fresh/runtime"; import { SidebarCategory } from "../../components/DocsSidebar.tsx"; import Footer from "../../components/Footer.tsx"; import Header from "../../components/Header.tsx"; import * as Icons from "../../components/Icons.tsx"; import { CATEGORIES, getFirstPageUrl, LATEST_VERSION, TABLE_OF_CONTENTS, type TableOfContentsEntry, } from "../../data/docs.ts"; import { frontMatter, renderMarkdown } from "../../utils/markdown.ts"; import toc from "../../../docs/toc.ts"; import { TableOfContents } from "../../islands/TableOfContents.tsx"; import SearchButton from "../../islands/SearchButton.tsx"; import VersionSelect from "../../islands/VersionSelect.tsx"; import { define } from "../../utils/state.ts"; interface Data { page: Page; } interface NavEntry { title: string; category?: string; href: string; } export interface VersionLink { label: string; href: string; value: string; } interface Page extends TableOfContentsEntry { markdown: string; data: Record; versionLinks: VersionLink[]; version: string; prevNav?: NavEntry; nextNav?: NavEntry; } const pattern = new URLPattern({ pathname: "/:version/:page*" }); export const handler = define.handlers({ async GET(ctx) { const slug = ctx.params.slug; // Check if the slug is the index page of a version tag if (TABLE_OF_CONTENTS[slug]) { const href = getFirstPageUrl(slug); return new Response("", { status: 307, headers: { location: href }, }); } const match = pattern.exec("https://localhost/" + slug); if (!match) { throw new HttpError(404); } let { version, page: path = "" } = match.pathname.groups; if (!version) { throw new HttpError(404); } // Latest version doesn't show up in the url if (!TABLE_OF_CONTENTS[version]) { path = version + (path ? "/" + path : ""); version = LATEST_VERSION; } // Check if the page exists const currentToc = TABLE_OF_CONTENTS[version]; const entry = currentToc[path]; if (!entry) { throw new HttpError(404); } // Build up the link map for the version selector. const versionLinks: VersionLink[] = []; for (const version in TABLE_OF_CONTENTS) { const label = toc[version].label; const maybeEntry = TABLE_OF_CONTENTS[version][path]; // Check if the same page is available for this version and // link to that. Pick the index page for that version if an // exact match doesn't exist. versionLinks.push({ label, value: version, href: maybeEntry ? maybeEntry.href : getFirstPageUrl(version), }); } // Add previous and next page entry if available const entryKeys = Object.keys(currentToc); const idx = entryKeys.findIndex((name) => name === entry.slug); let nextNav: NavEntry | undefined; let prevNav: NavEntry | undefined; const prevEntry = currentToc[entryKeys[idx - 1]]; const nextEntry = currentToc[entryKeys[idx + 1]]; if (prevEntry) { let category = prevEntry.category; category = category ? currentToc[category].title : ""; prevNav = { title: prevEntry.title, category, href: prevEntry.href }; } if (nextEntry) { let category = nextEntry.category; category = category ? currentToc[category].title : ""; nextNav = { title: nextEntry.title, category, href: nextEntry.href }; } // Parse markdown front matter // deno-lint-ignore no-explicit-any const url = (import.meta as any).env.PROD ? new URL(`../${entry.file}`, import.meta.url) : new URL(`../../../${entry.file}`, import.meta.url); const fileContent = await Deno.readTextFile(url); const { body, attrs } = frontMatter>(fileContent); ctx.state.title = `${entry.title ?? "Not Found"} | Fresh docs`; ctx.state.description = attrs?.description ? String(attrs.description) : "Fresh Document"; ctx.state.ogImage = new URL(asset("/og-image.webp"), ctx.url).href; return page({ page: { ...entry, markdown: body, data: attrs ?? {}, versionLinks, version, prevNav, nextNav, }, }); }, }); export default define.page(function DocsPage(props) { const { page } = props.data; const { html, headings } = renderMarkdown(page.markdown); const isCanary = page.href.includes("/canary"); return (
    {isCanary ? (
    🚧 This documentation is work in progress and for an unreleased version of Fresh.
    ) : null}

    {page.title}

    ); }); function MobileSidebar({ page }: { page: Page }) { return (
    ); } function ForwardBackButtons(props: { slug: string; version: string; prev?: NavEntry; next?: NavEntry; }) { const { prev, next } = props; return (
    {prev ? ( Previous page {prev.title} ) :
    } {next ? ( Next page {next.title} ) :
    }
    ); } ================================================ FILE: www/routes/docs/_layout.tsx ================================================ import type { PageProps } from "@fresh/core"; export default function Layout({ Component }: PageProps) { return (
    ); } ================================================ FILE: www/routes/docs/_middleware.ts ================================================ import type { Context } from "fresh"; const REDIRECTS: Record = { "/docs/getting-started/fetching-data": "/docs/getting-started/custom-handlers", "/docs/canary/examples/modifying-the-head": "/docs/advanced/head", "/docs/canary/concepts/builder": "/docs/advanced/builder", }; export async function handler(ctx: Context) { // Redirect from old doc URLs to new ones const redirect = REDIRECTS[ctx.url.pathname]; if (redirect) { const url = new URL(redirect, ctx.url.origin); return new Response("", { status: 307, headers: { location: url.href }, }); } return await ctx.next(); } ================================================ FILE: www/routes/docs/index.tsx ================================================ import { define } from "../../utils/state.ts"; export const handler = define.handlers({ GET(ctx) { return ctx.url.pathname === "/concepts/architechture" ? ctx.redirect("/docs/concepts/architecture") : ctx.redirect("/docs/introduction"); }, }); ================================================ FILE: www/routes/index.tsx ================================================ import { asset } from "fresh/runtime"; import { page } from "fresh"; import VERSIONS from "../../versions.json" with { type: "json" }; import Footer from "../components/Footer.tsx"; import Header from "../components/Header.tsx"; import { CTA } from "../components/homepage/CTA.tsx"; import { Hero } from "../components/homepage/Hero.tsx"; import { IslandsSection } from "../components/homepage/IslandsSection.tsx"; import { PartialsSection } from "../components/homepage/PartialsSection.tsx"; import { RenderingSection } from "../components/homepage/RenderingSection.tsx"; import { FormsSection } from "../components/homepage/FormsSection.tsx"; import { Simple } from "../components/homepage/Simple.tsx"; import { SocialProof } from "../components/homepage/SocialProof.tsx"; import { DenoSection } from "../components/homepage/DenoSection.tsx"; import { define } from "../utils/state.ts"; export const handler = define.handlers({ GET(ctx) { const { req } = ctx; const accept = req.headers.get("accept"); const userAgent = req.headers.get("user-agent"); if (userAgent?.includes("Deno/") && !accept?.includes("text/html")) { const path = `https://deno.land/x/fresh@${VERSIONS[0]}/init.ts`; return new Response(`Redirecting to ${path}`, { headers: { "Location": path }, status: 307, }); } ctx.state.title = "Fresh - The simple, approachable, productive web framework."; ctx.state.description = "Fresh features just-in-time edge rendering, island based interactivity, and zero-configuration TypeScript support. Fast to write; fast to run."; ctx.state.ogImage = new URL(asset("/og-image.webp"), ctx.url).href; return page(); }, async POST(ctx) { const headers = new Headers(); const form = await ctx.req.formData(); const treat = form.get("treat"); headers.set("location", `/thanks?vote=${treat}`); return new Response(null, { status: 303, headers, }); }, }); export default define.page(function MainPage() { return (
    ); }); function HelloBar() { return ( Fresh 2 released with Vite{" "} ); } ================================================ FILE: www/routes/raw.ts ================================================ import type { RouteConfig } from "fresh"; import { format, parse } from "@std/semver"; import VERSIONS from "../../versions.json" with { type: "json" }; import { extname } from "@std/path"; import { define } from "../utils/state.ts"; const BASE_URL = "https://raw.githubusercontent.com/denoland/fresh/"; const contentTypes = new Map([ [".html", "text/plain"], [".ts", "application/typescript"], [".js", "application/javascript"], [".tsx", "text/tsx"], [".jsx", "text/jsx"], [".json", "application/json"], [".wasm", "application/wasm"], ]); export const handler = define.handlers({ async GET(ctx) { const accept = ctx.req.headers.get("Accept"); const isHTML = accept?.includes("text/html"); const { version, path } = ctx.params; const semver = parse(version); if (!semver) { return new Response("Invalid version", { status: 400 }); } if (!VERSIONS.includes(format(semver))) { return new Response("Version not found", { status: 404 }); } const url = `${BASE_URL}${format(semver)}/${path}`; const r = await fetch(url, { redirect: "manual" }); const response = new Response(r.body, r); response.headers.delete("content-encoding"); if (response.status === 404) { return new Response( "404: Not Found. The requested Fresh release or file do not exist.", { status: 404 }, ); } if (!response.ok) { response.headers.set("Content-Type", "text/plain"); return response; } const value = isHTML ? "text/plain" : contentTypes.get(extname(path)) ?? "text/plain"; response.headers.set("Content-Type", value); return response; }, }); export const config: RouteConfig = { routeOverride: "/@:version/:path*", }; ================================================ FILE: www/routes/recipes/_layout.tsx ================================================ import type { PageProps } from "fresh"; import Header from "../../components/Header.tsx"; export default function Layout({ Component }: PageProps) { return ( <>

    This route is used only for demo purposes;{" "} see the home page.

    ); } ================================================ FILE: www/routes/recipes/_middleware.ts ================================================ import { define } from "../../utils/state.ts"; export const handler = define.middleware((ctx) => { ctx.state.noIndex = true; return ctx.next(); }); ================================================ FILE: www/routes/recipes/lemon-honey-tea.tsx ================================================ import { Partial } from "fresh/runtime"; import type { RouteConfig } from "fresh"; export const config: RouteConfig = { skipAppWrapper: true, skipInheritedLayouts: true, }; export const LemonHoneyTea = () => (

    Lemon-honey tea

    • 1 cup boiling water
    • ¼ tsp black tea
    • 2 tsp honey
    • 1 tsp lemon juice
    • Lemon peel
    ); export default LemonHoneyTea; ================================================ FILE: www/routes/recipes/lemonade.tsx ================================================ import { Partial } from "fresh/runtime"; import type { RouteConfig } from "fresh"; export const config: RouteConfig = { skipAppWrapper: true, skipInheritedLayouts: true, }; export const Lemonade = () => (

    Lemonade

    • 1 ¾ cups white sugar
    • 1 cup water
    • 9 lemons
    • 7 cups ice water
    • Ice
    ); export default Lemonade; ================================================ FILE: www/routes/recipes/lemondrop.tsx ================================================ import { Partial } from "fresh/runtime"; import type { RouteConfig } from "fresh"; export const config: RouteConfig = { skipAppWrapper: true, skipInheritedLayouts: true, }; export const LemonDrop = () => (

    Lemondrop Martini

    • 2 oz vodka
    • 3/4 oz triple sec
    • 1 oz fresh lemon juice
    • 3/4 oz simple syrup
    ); export default LemonDrop; ================================================ FILE: www/routes/showcase-bak.tsx ================================================ import { asset } from "fresh/runtime"; import { page } from "fresh"; import Projects, { type Project } from "../components/Projects.tsx"; import Header from "../components/Header.tsx"; import Footer from "../components/Footer.tsx"; import projects from "../data/showcase.json" with { type: "json" }; import { define } from "../utils/state.ts"; const TITLE = "Showcase | Fresh"; const DESCRIPTION = "Selection of projects that have been built with Fresh."; export const handler = define.handlers({ GET(ctx) { ctx.state.title = TITLE; ctx.state.description = DESCRIPTION; ctx.state.ogImage = new URL(asset("/og-image.webp"), ctx.url).href; return page(); }, }); export default define.page(function ShowcasePage() { return ( <>

    Badge

    You can add these stylish badges to your project’s README to show that it was built with Fresh.

    Made with Fresh Made with Fresh

    Usage instructions

    a deno plush is holding a lemon
    ); }); function Showcase({ items }: { items: Project[] }) { return (

    Showcase

    Below is a selection of projects that have been built with Fresh.{" "} Add yours!

    ); } ================================================ FILE: www/routes/showcase.tsx ================================================ import { asset } from "fresh/runtime"; import { page } from "fresh"; import Projects, { type Project } from "../components/Projects.tsx"; import Header from "../components/Header.tsx"; import Footer from "../components/Footer.tsx"; import projects from "../data/showcase.json" with { type: "json" }; import { define } from "../utils/state.ts"; const TITLE = "Showcase | Fresh"; const DESCRIPTION = "Selection of projects that have been built with Fresh."; export const handler = define.handlers({ GET(ctx) { ctx.state.title = TITLE; ctx.state.description = DESCRIPTION; ctx.state.ogImage = new URL(asset("/og-image.webp"), ctx.url).href; return page(); }, }); export default define.page(function ShowcasePage() { return (

    Badge

    You can add these stylish badges to your project’s README to show that it was built with Fresh.

    Made with Fresh Made with Fresh

    Usage instructions

    a deno plush is holding a lemon
    ); }); function Showcase({ items }: { items: Project[] }) { return (

    Showcase

    Below is a selection of projects that have been built with Fresh.{" "} Add yours!

    ); } ================================================ FILE: www/routes/thanks.tsx ================================================ import { page } from "fresh"; import { PageSection } from "../components/PageSection.tsx"; import { DemoBox } from "../components/homepage/DemoBox.tsx"; import { define } from "../utils/state.ts"; export const handler = define.handlers({ GET(ctx) { const search = new URLSearchParams(ctx.url.search); const vote = search.get("vote"); return page({ vote }); }, }); export default define.page(function ThanksForSubscribing( props, ) { const vote = props.data.vote ? props.data.vote.replaceAll(/-/g, " ") : null; return ( <>

    {vote ? `Thanks for voting for ${vote}!` : `Form submitted successfully`}

    That was all handled server-side, with{" "} no client-side JavaScript! Nifty, huh?

    …Anyway, you probably want to{" "} go back now.

    Back
    ); }); ================================================ FILE: www/routes/update.tsx ================================================ import { define } from "../utils/state.ts"; import VERSIONS from "../../versions.json" with { type: "json" }; export const handler = define.handlers({ GET({ req }) { const accept = req.headers.get("accept"); let path = "/docs/concepts/updating"; if (accept && !accept.includes("text/html")) { path = `https://deno.land/x/fresh@${VERSIONS[0]}/update.ts`; } return new Response(`Redirecting to ${path}`, { headers: { "Location": path }, status: 307, }); }, }); ================================================ FILE: www/static/docsearch.css ================================================ /** * Skipped minification because the original files appears to be already minified. * Original file: /npm/@docsearch/css@3.3.0/dist/style.css * * Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files */ /*! @docsearch/css 3.3.0 | MIT License | © Algolia, Inc. and contributors | https://docsearch.algolia.com */ :root { --docsearch-primary-color: #5468ff; --docsearch-text-color: #1c1e21; --docsearch-spacing: 12px; --docsearch-icon-stroke-width: 1.4; --docsearch-highlight-color: var(--docsearch-primary-color); --docsearch-muted-color: #969faf; --docsearch-container-background: rgba(101, 108, 133, 0.8); --docsearch-logo-color: #5468ff; --docsearch-modal-width: 560px; --docsearch-modal-height: 600px; --docsearch-modal-background: #f5f6f7; --docsearch-modal-shadow: inset 1px 1px 0 0 hsla(0, 0%, 100%, 0.5),0 3px 8px 0 #555a64; --docsearch-searchbox-height: 56px; --docsearch-searchbox-background: #ebedf0; --docsearch-searchbox-focus-background: #fff; --docsearch-searchbox-shadow: inset 0 0 0 2px var(--docsearch-primary-color); --docsearch-hit-height: 56px; --docsearch-hit-color: #444950; --docsearch-hit-active-color: #fff; --docsearch-hit-background: #fff; --docsearch-hit-shadow: 0 1px 3px 0 #d4d9e1; --docsearch-key-gradient: linear-gradient(-225deg, #d5dbe4, #f8f8f8); --docsearch-key-shadow: inset 0 -2px 0 0 #cdcde6,inset 0 0 1px 1px #fff,0 1px 2px 1px rgba( 30, 35, 90, 0.4 ); --docsearch-footer-height: 44px; --docsearch-footer-background: #fff; --docsearch-footer-shadow: 0 -1px 0 0 #e0e3e8,0 -3px 6px 0 rgba(69, 98, 155, 0.12); } html[data-theme="dark"] { --docsearch-text-color: #f5f6f7; --docsearch-container-background: rgba(9, 10, 17, 0.8); --docsearch-modal-background: #15172a; --docsearch-modal-shadow: inset 1px 1px 0 0 #2c2e40,0 3px 8px 0 #000309; --docsearch-searchbox-background: #2b2d3c; --docsearch-searchbox-focus-background: #000; --docsearch-hit-color: #bec3c9; --docsearch-hit-shadow: none; --docsearch-hit-background: #090a11; --docsearch-key-gradient: linear-gradient(-26.5deg, #565872, #31355b); --docsearch-key-shadow: inset 0 -2px 0 0 #282d55,inset 0 0 1px 1px #51577d,0 2px 2px 0 rgba( 3, 4, 9, 0.3 ); --docsearch-footer-background: #1e2136; --docsearch-footer-shadow: inset 0 1px 0 0 rgba(73, 76, 106, 0.5),0 -4px 8px 0 rgba(0, 0, 0, 0.2); --docsearch-logo-color: #fff; --docsearch-muted-color: #7f8497; } .DocSearch-Button { align-items: center; background: var(--docsearch-searchbox-background); border: 0; border-radius: 40px; color: var(--docsearch-muted-color); cursor: pointer; display: flex; font-weight: 500; height: 36px; justify-content: space-between; margin: 0 0 0 16px; padding: 0 8px; user-select: none; } .DocSearch-Button:active, .DocSearch-Button:focus, .DocSearch-Button:hover { background: var(--docsearch-searchbox-focus-background); box-shadow: var(--docsearch-searchbox-shadow); color: var(--docsearch-text-color); outline: none; } .DocSearch-Button-Container { align-items: center; display: flex; } .DocSearch-Search-Icon { stroke-width: 1.6; } .DocSearch-Button .DocSearch-Search-Icon { color: var(--docsearch-text-color); } .DocSearch-Button-Placeholder { font-size: 1rem; padding: 0 12px 0 6px; } .DocSearch-Button-Keys { display: flex; min-width: calc(40px + 0.8em); } .DocSearch-Button-Key { align-items: center; background: var(--docsearch-key-gradient); border-radius: 3px; box-shadow: var(--docsearch-key-shadow); color: var(--docsearch-muted-color); display: flex; height: 18px; justify-content: center; margin-right: 0.4em; position: relative; padding: 0 0 2px; border: 0; top: -1px; width: 20px; } @media (max-width: 768px) { .DocSearch-Button-Keys, .DocSearch-Button-Placeholder { display: none; } } .DocSearch--active { overflow: hidden !important; } .DocSearch-Container, .DocSearch-Container * { box-sizing: border-box; } .DocSearch-Container { background-color: var(--docsearch-container-background); height: 100vh; left: 0; position: fixed; top: 0; width: 100vw; z-index: 200; } .DocSearch-Container a { text-decoration: none; } .DocSearch-Link { appearance: none; background: none; border: 0; color: var(--docsearch-highlight-color); cursor: pointer; font: inherit; margin: 0; padding: 0; } .DocSearch-Modal { background: var(--docsearch-modal-background); border-radius: 6px; box-shadow: var(--docsearch-modal-shadow); flex-direction: column; margin: 60px auto auto; max-width: var(--docsearch-modal-width); position: relative; } .DocSearch-SearchBar { display: flex; padding: var(--docsearch-spacing) var(--docsearch-spacing) 0; } .DocSearch-Form { align-items: center; background: var(--docsearch-searchbox-focus-background); border-radius: 4px; box-shadow: var(--docsearch-searchbox-shadow); display: flex; height: var(--docsearch-searchbox-height); margin: 0; padding: 0 var(--docsearch-spacing); position: relative; width: 100%; } .DocSearch-Input { appearance: none; background: transparent; border: 0; color: var(--docsearch-text-color); flex: 1; font: inherit; font-size: 1.2em; height: 100%; outline: none; padding: 0 0 0 8px; width: 80%; } .DocSearch-Input::placeholder { color: var(--docsearch-muted-color); opacity: 1; } .DocSearch-Input::-webkit-search-cancel-button, .DocSearch-Input::-webkit-search-decoration, .DocSearch-Input::-webkit-search-results-button, .DocSearch-Input::-webkit-search-results-decoration { display: none; } .DocSearch-LoadingIndicator, .DocSearch-MagnifierLabel, .DocSearch-Reset { margin: 0; padding: 0; } .DocSearch-MagnifierLabel, .DocSearch-Reset { align-items: center; color: var(--docsearch-highlight-color); display: flex; justify-content: center; } .DocSearch-Container--Stalled .DocSearch-MagnifierLabel, .DocSearch-LoadingIndicator { display: none; } .DocSearch-Container--Stalled .DocSearch-LoadingIndicator { align-items: center; color: var(--docsearch-highlight-color); display: flex; justify-content: center; } @media screen and (prefers-reduced-motion: reduce) { .DocSearch-Reset { animation: none; appearance: none; background: none; border: 0; border-radius: 50%; color: var(--docsearch-icon-color); cursor: pointer; right: 0; stroke-width: var(--docsearch-icon-stroke-width); } } .DocSearch-Reset { animation: fade-in 0.1s ease-in forwards; appearance: none; background: none; border: 0; border-radius: 50%; color: var(--docsearch-icon-color); cursor: pointer; padding: 2px; right: 0; stroke-width: var(--docsearch-icon-stroke-width); } .DocSearch-Reset[hidden] { display: none; } .DocSearch-Reset:focus { outline: none; } .DocSearch-Reset:hover { color: var(--docsearch-highlight-color); } .DocSearch-LoadingIndicator svg, .DocSearch-MagnifierLabel svg { height: 24px; width: 24px; } .DocSearch-Cancel { display: none; } .DocSearch-Dropdown { max-height: calc( var(--docsearch-modal-height) - var(--docsearch-searchbox-height) - var(--docsearch-spacing) - var(--docsearch-footer-height) ); min-height: var(--docsearch-spacing); overflow-y: auto; overflow-y: overlay; padding: 0 var(--docsearch-spacing); scrollbar-color: var(--docsearch-muted-color) var(--docsearch-modal-background); scrollbar-width: thin; } .DocSearch-Dropdown::-webkit-scrollbar { width: 12px; } .DocSearch-Dropdown::-webkit-scrollbar-track { background: transparent; } .DocSearch-Dropdown::-webkit-scrollbar-thumb { background-color: var(--docsearch-muted-color); border: 3px solid var(--docsearch-modal-background); border-radius: 20px; } .DocSearch-Dropdown ul { list-style: none; margin: 0; padding: 0; } .DocSearch-Label { font-size: 0.75em; line-height: 1.6em; } .DocSearch-Help, .DocSearch-Label { color: var(--docsearch-muted-color); } .DocSearch-Help { font-size: 0.9em; margin: 0; user-select: none; } .DocSearch-Title { font-size: 1.2em; } .DocSearch-Logo a { display: flex; } .DocSearch-Logo svg { color: var(--docsearch-logo-color); margin-left: 8px; } .DocSearch-Hits:last-of-type { margin-bottom: 24px; } .DocSearch-Hits mark { background: none; color: var(--docsearch-highlight-color); } .DocSearch-HitsFooter { color: var(--docsearch-muted-color); display: flex; font-size: 0.85em; justify-content: center; margin-bottom: var(--docsearch-spacing); padding: var(--docsearch-spacing); } .DocSearch-HitsFooter a { border-bottom: 1px solid; color: inherit; } .DocSearch-Hit { border-radius: 4px; display: flex; padding-bottom: 4px; position: relative; } @media screen and (prefers-reduced-motion: reduce) { .DocSearch-Hit--deleting { transition: none; } } .DocSearch-Hit--deleting { opacity: 0; transition: all 0.25s linear; } @media screen and (prefers-reduced-motion: reduce) { .DocSearch-Hit--favoriting { transition: none; } } .DocSearch-Hit--favoriting { transform: scale(0); transform-origin: top center; transition: all 0.25s linear; transition-delay: 0.25s; } .DocSearch-Hit a { background: var(--docsearch-hit-background); border-radius: 4px; box-shadow: var(--docsearch-hit-shadow); display: block; padding-left: var(--docsearch-spacing); width: 100%; } .DocSearch-Hit-source { background: var(--docsearch-modal-background); color: var(--docsearch-highlight-color); font-size: 0.85em; font-weight: 600; line-height: 32px; margin: 0 -4px; padding: 8px 4px 0; position: sticky; top: 0; z-index: 10; } .DocSearch-Hit-Tree { color: var(--docsearch-muted-color); height: var(--docsearch-hit-height); opacity: 0.5; stroke-width: var(--docsearch-icon-stroke-width); width: 24px; } .DocSearch-Hit[aria-selected="true"] a { background-color: var(--docsearch-highlight-color); } .DocSearch-Hit[aria-selected="true"] mark { text-decoration: underline; } .DocSearch-Hit-Container { align-items: center; color: var(--docsearch-hit-color); display: flex; flex-direction: row; height: var(--docsearch-hit-height); padding: 0 var(--docsearch-spacing) 0 0; } .DocSearch-Hit-icon { height: 20px; width: 20px; } .DocSearch-Hit-action, .DocSearch-Hit-icon { color: var(--docsearch-muted-color); stroke-width: var(--docsearch-icon-stroke-width); } .DocSearch-Hit-action { align-items: center; display: flex; height: 22px; width: 22px; } .DocSearch-Hit-action svg { display: block; height: 18px; width: 18px; } .DocSearch-Hit-action + .DocSearch-Hit-action { margin-left: 6px; } .DocSearch-Hit-action-button { appearance: none; background: none; border: 0; border-radius: 50%; color: inherit; cursor: pointer; padding: 2px; } svg.DocSearch-Hit-Select-Icon { display: none; } .DocSearch-Hit[aria-selected="true"] .DocSearch-Hit-Select-Icon { display: block; } .DocSearch-Hit-action-button:focus, .DocSearch-Hit-action-button:hover { background: rgba(0, 0, 0, 0.2); transition: background-color 0.1s ease-in; } @media screen and (prefers-reduced-motion: reduce) { .DocSearch-Hit-action-button:focus, .DocSearch-Hit-action-button:hover { transition: none; } } .DocSearch-Hit-action-button:focus path, .DocSearch-Hit-action-button:hover path { fill: #fff; } .DocSearch-Hit-content-wrapper { display: flex; flex: 1 1 auto; flex-direction: column; font-weight: 500; justify-content: center; line-height: 1.2em; margin: 0 8px; overflow-x: hidden; position: relative; text-overflow: ellipsis; white-space: nowrap; width: 80%; } .DocSearch-Hit-title { font-size: 0.9em; } .DocSearch-Hit-path { color: var(--docsearch-muted-color); font-size: 0.75em; } .DocSearch-Hit[aria-selected="true"] .DocSearch-Hit-action, .DocSearch-Hit[aria-selected="true"] .DocSearch-Hit-icon, .DocSearch-Hit[aria-selected="true"] .DocSearch-Hit-path, .DocSearch-Hit[aria-selected="true"] .DocSearch-Hit-text, .DocSearch-Hit[aria-selected="true"] .DocSearch-Hit-title, .DocSearch-Hit[aria-selected="true"] .DocSearch-Hit-Tree, .DocSearch-Hit[aria-selected="true"] mark { color: var(--docsearch-hit-active-color) !important; } @media screen and (prefers-reduced-motion: reduce) { .DocSearch-Hit-action-button:focus, .DocSearch-Hit-action-button:hover { background: rgba(0, 0, 0, 0.2); transition: none; } } .DocSearch-ErrorScreen, .DocSearch-NoResults, .DocSearch-StartScreen { font-size: 0.9em; margin: 0 auto; padding: 36px 0; text-align: center; width: 80%; } .DocSearch-Screen-Icon { color: var(--docsearch-muted-color); padding-bottom: 12px; } .DocSearch-NoResults-Prefill-List { display: inline-block; padding-bottom: 24px; text-align: left; } .DocSearch-NoResults-Prefill-List ul { display: inline-block; padding: 8px 0 0; } .DocSearch-NoResults-Prefill-List li { list-style-position: inside; list-style-type: "» "; } .DocSearch-Prefill { appearance: none; background: none; border: 0; border-radius: 1em; color: var(--docsearch-highlight-color); cursor: pointer; display: inline-block; font-size: 1em; font-weight: 700; padding: 0; } .DocSearch-Prefill:focus, .DocSearch-Prefill:hover { outline: none; text-decoration: underline; } .DocSearch-Footer { align-items: center; background: var(--docsearch-footer-background); border-radius: 0 0 8px 8px; box-shadow: var(--docsearch-footer-shadow); display: flex; flex-direction: row-reverse; flex-shrink: 0; height: var(--docsearch-footer-height); justify-content: space-between; padding: 0 var(--docsearch-spacing); position: relative; user-select: none; width: 100%; z-index: 300; } .DocSearch-Commands { color: var(--docsearch-muted-color); display: flex; list-style: none; margin: 0; padding: 0; } .DocSearch-Commands li { align-items: center; display: flex; } .DocSearch-Commands li:not(:last-of-type) { margin-right: 0.8em; } .DocSearch-Commands-Key { align-items: center; background: var(--docsearch-key-gradient); border-radius: 2px; box-shadow: var(--docsearch-key-shadow); display: flex; height: 18px; justify-content: center; margin-right: 0.4em; padding: 0 0 1px; color: var(--docsearch-muted-color); border: 0; width: 20px; } @media (max-width: 768px) { :root { --docsearch-spacing: 10px; --docsearch-footer-height: 40px; } .DocSearch-Dropdown { height: 100%; } .DocSearch-Container { height: 100vh; height: -webkit-fill-available; height: calc(var(--docsearch-vh, 1vh) * 100); position: absolute; } .DocSearch-Footer { border-radius: 0; bottom: 0; position: absolute; } .DocSearch-Hit-content-wrapper { display: flex; position: relative; width: 80%; } .DocSearch-Modal { border-radius: 0; box-shadow: none; height: 100vh; height: -webkit-fill-available; height: calc(var(--docsearch-vh, 1vh) * 100); margin: 0; max-width: 100%; width: 100%; } .DocSearch-Dropdown { max-height: calc( var(--docsearch-vh, 1vh) * 100 - var(--docsearch-searchbox-height) - var(--docsearch-spacing) - var(--docsearch-footer-height) ); } .DocSearch-Cancel { appearance: none; background: none; border: 0; color: var(--docsearch-highlight-color); cursor: pointer; display: inline-block; flex: none; font: inherit; font-size: 1em; font-weight: 500; margin-left: var(--docsearch-spacing); outline: none; overflow: hidden; padding: 0; user-select: none; white-space: nowrap; } .DocSearch-Commands, .DocSearch-Hit-Tree { display: none; } } @keyframes fade-in { 0% { opacity: 0; } to { opacity: 1; } } /* css for docs */ .DocSearch-Button { margin: 0; width: 100%; border-radius: 0.5rem; } .DocSearch-Button-Placeholder { display: initial; } .DocSearch-Container { cursor: auto; } ================================================ FILE: www/static/google40caa9e535ae39e9.html ================================================ google-site-verification: google40caa9e535ae39e9.html ================================================ FILE: www/static/markdown.css ================================================ :root { --color-canvas-default-transparent: rgba(255, 255, 255, 0); --color-prettylights-syntax-comment: #6e7781; --color-prettylights-syntax-constant: #0550ae; --color-prettylights-syntax-entity: #8250df; --color-prettylights-syntax-storage-modifier-import: var( --color-foreground-secondary ); --color-prettylights-syntax-entity-tag: #22863a; --color-prettylights-syntax-keyword: #cf222e; --color-prettylights-syntax-string: var(--color-prettylights-syntax-constant); --color-prettylights-syntax-variable: #953800; --color-prettylights-syntax-string-regexp: #116329; --color-prettylights-syntax-markup-deleted-text: #82071e; --color-prettylights-syntax-markup-deleted-bg: #ffebe9; --color-prettylights-syntax-markup-inserted-text: #116329; --color-prettylights-syntax-markup-inserted-bg: #dafbe1; --color-prettylights-syntax-constant-other-reference-link: #0a3069; --color-fg-default: var(--color-foreground-secondary); --color-fg-muted: var(--color-foreground-tertiary); --color-canvas-default: var(--color-background-primary); --color-canvas-subtle: var(--color-background-secondary); --color-border-default: hsl( from var(--color-foreground-secondary) h s l / 0.3 ); --color-border-muted: hsl(from var(--color-foreground-secondary) h s l / 0.1); --color-neutral-muted: rgba(175, 184, 193, 0.2); --color-accent-fg: #02aa6f; --color-accent-emphasis: #0969da; --color-danger-fg: #cf222e; --warn-border: #ff9100; --warn-bg: #f0900525; --warn-text: var(--color-fg-default); } html[data-theme="dark"] { --color-accent-fg: #75eea1; --color-prettylights-syntax-keyword: #f86d76; --color-prettylights-syntax-entity: #b392f0; --color-prettylights-syntax-constant: #8fc0fb; --color-prettylights-syntax-entity-tag: #7cb78a; --color-prettylights-syntax-variable: #f7955c; --color-prettylights-syntax-markup-deleted-text: #ffebe9; --color-prettylights-syntax-markup-deleted-bg: #82071e; --color-prettylights-syntax-markup-inserted-text: #dafbe1; --color-prettylights-syntax-markup-inserted-bg: #116329; --warn-border: #ff9100; --warn-bg: #f0900525; --warn-text: var(--color-fg-default); } .markdown-body { word-wrap: break-word; font-family: inherit; font-size: 1rem; line-height: 1.75; } .markdown-body:before { content: ""; display: table; } .markdown-body:after { clear: both; content: ""; display: table; } .markdown-body > :first-child { margin-top: 0 !important; } .markdown-body > :last-child { margin-bottom: 0 !important; } .markdown-body a:not([href]) { color: inherit; text-decoration: none; } .markdown-body .absent { color: var(--color-danger-fg); } .markdown-body .anchor { float: left; margin-left: -20px; padding-right: 4px; line-height: 1; } .markdown-body .anchor:focus { outline: none; } .markdown-body p { margin: 1rem; } .markdown-body p, .markdown-body blockquote, .markdown-body ul, .markdown-body ol, .markdown-body dl, .markdown-body table, .markdown-body pre, .markdown-body details { margin-bottom: 1rem; } .markdown-body ul, .markdown-body ol { margin-top: 1rem; } .markdown-body hr { height: 0.25em; background-color: var(--color-border-default); border: 0; margin: 24px 0; padding: 0; } .markdown-body blockquote { color: var(--color-fg-default); border-left: 0.25em solid var(--color-border-default); padding: 0 1em; } .markdown-body blockquote > :first-child { margin-top: 0; } .markdown-body blockquote > :last-child { margin-bottom: 0; } .markdown-body h1, .markdown-body h2, .markdown-body h3, .markdown-body h4, .markdown-body h5, .markdown-body h6 { font-weight: var(--base-text-weight-semibold, 600); margin-top: 24px; margin-bottom: 16px; line-height: 1.25; } .markdown-body h1 .octicon-link, .markdown-body h2 .octicon-link, .markdown-body h3 .octicon-link, .markdown-body h4 .octicon-link, .markdown-body h5 .octicon-link, .markdown-body h6 .octicon-link { color: var(--color-fg-default); vertical-align: middle; visibility: hidden; } .markdown-body h1:hover .anchor, .markdown-body h2:hover .anchor, .markdown-body h3:hover .anchor, .markdown-body h4:hover .anchor, .markdown-body h5:hover .anchor, .markdown-body h6:hover .anchor { text-decoration: none; } .markdown-body h1:hover .anchor .octicon-link, .markdown-body h2:hover .anchor .octicon-link, .markdown-body h3:hover .anchor .octicon-link, .markdown-body h4:hover .anchor .octicon-link, .markdown-body h5:hover .anchor .octicon-link, .markdown-body h6:hover .anchor .octicon-link { visibility: visible; } .markdown-body h1 tt, .markdown-body h1 code, .markdown-body h2 tt, .markdown-body h2 code, .markdown-body h3 tt, .markdown-body h3 code, .markdown-body h4 tt, .markdown-body h4 code, .markdown-body h5 tt, .markdown-body h5 code, .markdown-body h6 tt, .markdown-body h6 code { font-size: inherit; padding: 0 0.2em; } .markdown-body h2 { border-top: 1px solid var(--color-border-muted); padding-top: 1.25rem; margin-top: 2rem; font-size: 1.5em; } .markdown-body h3 { font-size: 1.25em; } .markdown-body h4 { font-size: 1em; } .markdown-body h5 { font-size: 0.875em; } .markdown-body h6 { color: var(--color-fg-muted); font-size: 0.85em; } .markdown-body summary h1, .markdown-body summary h2, .markdown-body summary h3, .markdown-body summary h4, .markdown-body summary h5, .markdown-body summary h6 { display: inline-block; } .markdown-body summary h1 .anchor, .markdown-body summary h2 .anchor, .markdown-body summary h3 .anchor, .markdown-body summary h4 .anchor, .markdown-body summary h5 .anchor, .markdown-body summary h6 .anchor { margin-left: -40px; } .markdown-body summary h1, .markdown-body summary h2 { border-bottom: 0; padding-bottom: 0; } .markdown-body strong { font-weight: 600; } .markdown-body ul, .markdown-body ol { padding-left: 1em; } .markdown-body ul.no-list, .markdown-body ol.no-list { padding: 0; list-style-type: none; } .markdown-body ol[type="a"] { list-style-type: lower-alpha; } .markdown-body ol[type="A"] { list-style-type: upper-alpha; } .markdown-body ol[type="i"] { list-style-type: lower-roman; } .markdown-body ol[type="I"] { list-style-type: upper-roman; } .markdown-body ol[type="1"] { list-style-type: decimal; } .markdown-body div > ol:not([type]) { list-style-type: decimal; } .markdown-body ul ul, .markdown-body ul ol, .markdown-body ol ol, .markdown-body ol ul { margin-top: 0; margin-bottom: 0; } .markdown-body li > p { margin-top: 16px; } .markdown-body li + li { margin-top: 0.25em; } .markdown-body dl { padding: 0; } .markdown-body dl dt { font-size: 1em; font-style: italic; font-weight: var(--base-text-weight-semibold, 600); margin-top: 16px; padding: 0; } .markdown-body dl dd { margin-bottom: 16px; padding: 0 16px; } .markdown-body table { width: 100%; width: -webkit-max-content; width: -webkit-max-content; width: max-content; max-width: 100%; display: block; overflow: auto; } .markdown-body table th { font-weight: var(--base-text-weight-semibold, 600); } .markdown-body table th, .markdown-body table td { border: 1px solid var(--color-border-default); padding: 6px 13px; } .markdown-body table td > :last-child { margin-bottom: 0; } .markdown-body table tr { background-color: var(--color-canvas-default); border-top: 1px solid var(--color-border-muted); } .markdown-body table tr:nth-child(2n) { background-color: var(--color-canvas-subtle); } .markdown-body table img { background-color: transparent; } .markdown-body img { max-width: 100%; box-sizing: content-box; background-color: var(--color-canvas-default); } .markdown-body img[align="right"] { padding-left: 20px; } .markdown-body img[align="left"] { padding-right: 20px; } .markdown-body .emoji { max-width: none; vertical-align: text-top; background-color: transparent; } .markdown-body span.frame { display: block; overflow: hidden; } .markdown-body span.frame > span { float: left; width: auto; border: 1px solid var(--color-border-default); margin: 13px 0 0; padding: 7px; display: block; overflow: hidden; } .markdown-body span.frame span img { float: left; display: block; } .markdown-body span.frame span span { clear: both; color: var(--color-fg-default); padding: 5px 0 0; display: block; } .markdown-body span.align-center { clear: both; display: block; overflow: hidden; } .markdown-body span.align-center > span { text-align: center; margin: 13px auto 0; display: block; overflow: hidden; } .markdown-body span.align-center span img { text-align: center; margin: 0 auto; } .markdown-body span.align-right { clear: both; display: block; overflow: hidden; } .markdown-body span.align-right > span { text-align: right; margin: 13px 0 0; display: block; overflow: hidden; } .markdown-body span.align-right span img { text-align: right; margin: 0; } .markdown-body span.float-left { float: left; margin-right: 13px; display: block; overflow: hidden; } .markdown-body span.float-left span { margin: 13px 0 0; } .markdown-body span.float-right { float: right; margin-left: 13px; display: block; overflow: hidden; } .markdown-body span.float-right > span { text-align: right; margin: 13px auto 0; display: block; overflow: hidden; } .markdown-body code, .markdown-body tt { white-space: break-spaces; background-color: var(--color-neutral-muted); border-radius: 6px; margin: 0; padding: 0.1875rem 0.375rem; font-size: 0.875rem; } .markdown-body code br, .markdown-body tt br { display: none; } .markdown-body del code { -webkit-text-decoration: inherit; -webkit-text-decoration: inherit; text-decoration: inherit; } .markdown-body samp { font-size: 85%; } .markdown-body pre { word-wrap: normal; } .markdown-body pre code { font-size: 100%; } .markdown-body pre > code { word-break: normal; white-space: pre; background: 0 0; border: 0; margin: 0; padding: 0; } .markdown-body .highlight { margin-bottom: 16px; } .markdown-body .highlight pre { word-break: normal; margin-bottom: 0; } .markdown-body .highlight pre, .markdown-body pre { background-color: var(--color-canvas-subtle); border-radius: 6px; padding: 1.25rem 1.5rem; font-size: 85%; line-height: 1.75; overflow: auto; } .markdown-body pre code, .markdown-body pre tt { max-width: auto; line-height: inherit; word-wrap: normal; background-color: transparent; border: 0; margin: 0; padding: 0; display: inline; overflow: visible; } .markdown-body .csv-data td, .markdown-body .csv-data th { text-align: left; white-space: nowrap; padding: 5px; font-size: 0.75rem; line-height: 1; overflow: hidden; } .markdown-body .csv-data .blob-num { text-align: right; background: var(--color-canvas-default); border: 0; padding: 10px 8px 9px; } .markdown-body .csv-data tr { border-top: 0; } .markdown-body .csv-data th { font-weight: var(--base-text-weight-semibold, 600); background: var(--color-canvas-subtle); border-top: 0; } .markdown-body [data-footnote-ref]:before { content: "["; } .markdown-body [data-footnote-ref]:after { content: "]"; } .markdown-body .footnotes { color: var(--color-fg-muted); border-top: 1px solid var(--color-border-default); font-size: 0.75rem; } .markdown-body .footnotes ol { padding-left: 16px; } .markdown-body .footnotes ol ul { margin-top: 16px; padding-left: 16px; display: inline-block; } .markdown-body .footnotes li { position: relative; } .markdown-body .footnotes li:target:before { pointer-events: none; content: ""; border: 2px solid var(--color-accent-emphasis); border-radius: 6px; position: absolute; top: -8px; bottom: -8px; left: -24px; right: -8px; } .markdown-body .footnotes li:target { color: var(--color-fg-default); } .markdown-body .footnotes .data-footnote-backref g-emoji { font-family: monospace; } .markdown-body { background-color: var(--color-canvas-default); color: var(--color-fg-default); } .markdown-body a { color: var(--color-accent-fg); text-decoration: underline; text-underline-offset: 2px; } .markdown-body a:hover { text-decoration: underline; } .markdown-body img[align="center"] { margin: 0 auto; } .markdown-body iframe { background-color: #fff; border: 0; margin-bottom: 16px; } .markdown-body svg.octicon { fill: currentColor; } .markdown-body .anchor > .octicon { display: inline; } .markdown-body figcaption { text-align: center; padding-top: 2px; } .markdown-body .highlight .token.keyword, .gfm-highlight .token.keyword { color: var(--color-prettylights-syntax-keyword); } .highlight .atrule .rule { color: var(--color-prettylights-syntax-keyword); } .markdown-body .highlight .token.tag .token.class-name, .markdown-body .highlight .token.tag .token.script .token.punctuation, .gfm-highlight .token.tag .token.class-name, .gfm-highlight .token.tag .token.script .token.punctuation { color: var(--color-prettylights-syntax-storage-modifier-import); } .markdown-body .highlight .token.operator, .markdown-body .highlight .token.number, .markdown-body .highlight .token.boolean, .markdown-body .highlight .token.tag .token.punctuation, .markdown-body .highlight .token.tag .token.script .token.script-punctuation, .markdown-body .highlight .token.tag .token.attr-name, .gfm-highlight .token.operator, .gfm-highlight .token.number, .gfm-highlight .token.boolean, .gfm-highlight .token.tag .token.punctuation, .gfm-highlight .token.tag .token.script .token.script-punctuation, .gfm-highlight .token.tag .token.attr-name { color: var(--color-prettylights-syntax-constant); } .markdown-body .highlight .token.function, .gfm-highlight .token.function { color: var(--color-prettylights-syntax-entity); } .markdown-body .highlight .token.string, .gfm-highlight .token.string { color: var(--color-prettylights-syntax-string); } .markdown-body .highlight .token.comment, .gfm-highlight .token.comment { color: var(--color-prettylights-syntax-comment); } .markdown-body .highlight .token.class-name, .gfm-highlight .token.class-name { color: var(--color-prettylights-syntax-variable); } .markdown-body .highlight .token.regex, .gfm-highlight .token.regex { color: var(--color-prettylights-syntax-string); } .markdown-body .highlight .token.regex .regex-delimiter, .gfm-highlight .token.regex .regex-delimiter { color: var(--color-prettylights-syntax-constant); } .markdown-body .highlight .token.tag .token.tag, .markdown-body .highlight .token.property, .gfm-highlight .token.tag .token.tag, .gfm-highlight .token.property { color: var(--color-prettylights-syntax-entity-tag); } .markdown-body .highlight .token.deleted, .gfm-highlight .token.deleted { color: var(--color-prettylights-syntax-markup-deleted-text); background-color: var(--color-prettylights-syntax-markup-deleted-bg); } .markdown-body .highlight .token.inserted, .gfm-highlight .token.inserted { color: var(--color-prettylights-syntax-markup-inserted-text); background-color: var(--color-prettylights-syntax-markup-inserted-bg); } .markdown-body h1, .markdown-body h2, .markdown-body h3, .markdown-body h4, .markdown-body h5, .markdown-body h6, .markdown-body ul, .markdown-body ol, .markdown-body p, .markdown-body details, .markdown-body blockquote { margin-left: 1rem; margin-right: 1rem; } .markdown-body blockquote p { margin-left: 0; } .markdown-body .admonition { padding: 1rem; margin: 1.5rem; } .markdown-body .admonition-header { font-weight: bold; display: flex; align-items: center; font-size: 1rem; } .markdown-body .admonition .icon { display: inline-flex; margin-right: 0.25rem; width: 1.2em; height: 1.2em; margin-bottom: 0.25rem; } .markdown-body blockquote.warn { border-color: var(--warn-border); background: var(--warn-bg); color: var(--warn-text); } .markdown-body .admonition .fenced-code { margin-left: 0; margin-right: 0; border: 1px solid #d9d2d2; border-radius: 0.5rem 0.5rem 0.25rem 0.25rem; } .markdown-body .admonition .highlight { margin-bottom: 0; } .markdown-body .admonition.tip .admonition-header { color: rgb(0, 148, 0); } html[data-theme="dark"] .markdown-body .admonition.tip { background-color: #1f331f; border-color: var(--color-accent-fg); } html[data-theme="dark"] .markdown-body .admonition.tip .admonition-header { color: var(--color-accent-fg); } .markdown-body .admonition.tip { background-color: rgb(230, 246, 230); border-color: rgb(0, 148, 0); } .markdown-body .admonition.info .admonition-header { color: rgb(25 146 184); } .markdown-body .admonition.info { background-color: rgb(238, 249, 253); border-color: rgb(25 146 184); } html[data-theme="dark"] .markdown-body .admonition.info { background-color: hsl(194 76% 41% / 0.1); color: var(--color-foreground-primary); } .markdown-body .admonition.warn .admonition-header { color: #dd6f04; } .markdown-body .admonition.warn { color: var(--warn-text); background-color: var(--warn-bg); border-color: var(--warn-border); } @media screen and (min-width: 768px) { .markdown-body table { margin-left: 1rem; margin-right: 1rem; } .markdown-body details .fenced-code { margin-left: 0; margin-right: 0; } .markdown-body .admonition { margin-left: 0; margin-right: 0; } .markdown-body .fenced-code, .markdown-body ol, .markdown-body ul, .markdown-body h1, .markdown-body h2, .markdown-body h4, .markdown-body h5, .markdown-body h6, .markdown-body p { margin-left: 0; margin-right: 0; } .markdown-body h3 { margin: 2rem 0 1rem 0; } } ol.nested { counter-reset: item; } ol.nested li { display: block; } ol.nested li:before { font-feature-settings: "kern" 1, "tnum" 1; -webkit-font-feature-settings: "kern" 1, "tnum" 1; -ms-font-feature-settings: "kern" 1, "tnum" 1; -moz-font-feature-settings: "kern" 1, "tnum" 1; content: counters(item, ".") ". "; counter-increment: item; counter-increment: item; } .markdown-body ul { list-style: disc; } .markdown-body ol { list-style: numeric; } .markdown-body .md-anchor { color: inherit; text-decoration: none !important; } .markdown-body .md-anchor span { color: var(--color-accent-fg); display: inline-block; margin-left: 0.25rem; opacity: 0; visibility: hidden; } .markdown-body .md-anchor:hover span { opacity: 1; visibility: visible; } .markdown-body .highlight { border-radius: 0.5rem; } .toggle:checked + .toggled { display: block; } .fenced-code { margin-bottom: 1rem; } .fenced-code pre { margin-bottom: 0; } .fenced-code-header { border-top-left-radius: 0.5rem; border-top-right-radius: 0.5rem; padding-left: 0.5rem; padding-right: 0.5rem; background: var(--color-background-tertiary); display: flex; align-items: center; justify-content: space-between; } .fenced-code-header + pre { border-top-left-radius: 0 !important; border-top-right-radius: 0 !important; } .fenced-code-title { display: flex; padding-top: 0.5rem; padding-bottom: 0.5rem; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-size: 0.8125rem; line-height: 1; justify-content: space-between; align-items: center; gap: 0.625rem; } .fenced-code-title > img { background-color: transparent; margin: -0.2rem 0; width: 1.25rem; } .highlight-source-yml .atrule { color: var(--color-prettylights-syntax-entity); } .highlight-source-yml .string { color: var(--color-prettylights-syntax-string); } .markdown-body pre.highlight-source-txt-files { line-height: 1.35; } .highlight-source-txt-files .function { font-weight: 600; } ================================================ FILE: www/static/prism.css ================================================ /* PrismJS 1.29.0 https://prismjs.com/download.html#themes=prism-okaidia&languages=markup+css+clike+javascript */ code[class*="language-"], pre[class*="language-"] { color: #f8f8f2; background: 0 0; text-shadow: 0 1px rgba(0, 0, 0, 0.3); font-family: Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace; font-size: 1em; text-align: left; white-space: pre; word-spacing: normal; word-break: normal; word-wrap: normal; line-height: 1.5; -moz-tab-size: 4; -o-tab-size: 4; tab-size: 4; -webkit-hyphens: none; -moz-hyphens: none; -ms-hyphens: none; hyphens: none; } pre[class*="language-"] { padding: 1em; margin: 0.5em 0; overflow: auto; border-radius: 0.3em; } :not(pre) > code[class*="language-"], pre[class*="language-"] { background: #272822; } :not(pre) > code[class*="language-"] { padding: 0.1em; border-radius: 0.3em; white-space: normal; } .token.cdata, .token.comment, .token.doctype, .token.prolog { color: #8292a2; } .token.punctuation { color: #f8f8f2; } .token.namespace { opacity: 0.7; } .token.constant, .token.deleted, .token.property, .token.symbol, .token.tag { color: #f92672; } .token.boolean, .token.number { color: #ae81ff; } .token.attr-name, .token.builtin, .token.char, .token.inserted, .token.selector, .token.string { color: #a6e22e; } .language-css .token.string, .style .token.string, .token.entity, .token.operator, .token.url, .token.variable { color: #f8f8f2; } .token.atrule, .token.attr-value, .token.class-name, .token.function { color: #e6db74; } .token.keyword { color: #66d9ef; } .token.important, .token.regex { color: #fd971f; } .token.bold, .token.important { font-weight: 700; } .token.italic { font-style: italic; } .token.entity { cursor: help; } ================================================ FILE: www/utils/markdown.ts ================================================ export { extractYaml as frontMatter } from "@std/front-matter"; import * as Marked from "marked"; import { HttpError } from "fresh"; import { asset } from "fresh/runtime"; import { escape as escapeHtml } from "@std/html"; import { mangle } from "marked-mangle"; import GitHubSlugger from "github-slugger"; import { Prism } from "./prism.ts"; Marked.marked.use(mangle()); const ADMISSION_REG = /^\[(info|warn|tip)\]:\s/; const LOGOS = [ { lang: /^tsx?$/, file: /\.tsx?$/, src: asset("/logos/typescript.svg"), text: "Typescript", }, { lang: /^css$/, file: /\.css$/, src: asset("/logos/css.svg"), text: "CSS", }, { lang: /^html$/, file: /\.html$/, src: asset("/logos/html.svg"), text: "HTML", }, { lang: /^jsonc?$/, file: /\.jsonc?$/, src: asset("/logos/json.svg"), text: "JSON", }, { lang: /^(sh|bash)$/, file: /\.sh$/, src: asset("/logos/shell.svg"), text: "Terminal (Shell/Bash)", }, { lang: /^md$/, file: /\.md$/, src: asset("/logos/markdown.svg"), text: "Markdown", }, { lang: /^txt(-files)?$/, file: /\.txt$/, src: asset("/logos/text.svg"), text: "Text", }, { lang: /^diff$/, file: /\.diff$/, src: asset("/logos/diff.svg"), text: "File diff", }, { lang: /^gitignore$/, file: /^\.gitignore$/, src: asset("/logos/git.svg"), text: "Git", }, { lang: /^dockerfile$/, file: /^Dockerfile$/, src: asset("/logos/docker.svg"), text: "Docker", }, ]; export interface MarkdownHeading { id: string; html: string; level: number; } class DefaultRenderer extends Marked.Renderer { headings: MarkdownHeading[] = []; slugger = new GitHubSlugger(); override text( token: Marked.Tokens.Text | Marked.Tokens.Escape | Marked.Tokens.Tag, ): string { if ( token.type === "text" && "tokens" in token && token.tokens !== undefined ) { return this.parser.parseInline(token.tokens); } // Smartypants typography enhancement return token.text .replaceAll("...", "…") .replaceAll("--", "—") .replaceAll("---", "–") .replaceAll(/(\w)'(\w)/g, "$1’$2") .replaceAll(/s'/g, "s’") .replaceAll("'", "’") .replaceAll(/["](.*?)["]/g, "“$1”") .replaceAll(/"(.*?)"/g, "“$1”") .replaceAll(/['](.*?)[']/g, "‘$1’"); } override heading({ tokens, depth }: Marked.Tokens.Heading): string { this.#assert(tokens.length > 0, "Markdown heading tokens unexpected value"); let content = ""; for (let i = 0; i < tokens.length; i++) { const token = tokens[i]; switch (token.type) { case "text": content += token.text; continue; case "codespan": content += token.text.replaceAll(/[<>]/g, ""); continue; default: this.#assert( false, "Markdown heading tokens unexpected value", ); } } let slugInput = content; // Rewrites e.g. `.get()` to `get` if (/^\..*\(\)$/.test(slugInput)) { slugInput = slugInput.slice(1, -2); } const slug = this.slugger.slug(slugInput); const text = this.parser.parseInline(tokens); this.headings.push({ id: slug, html: text, level: depth }); return `${text}`; } override link({ href, title, tokens }: Marked.Tokens.Link) { const text = this.parser.parseInline(tokens); const titleAttr = title ? ` title="${title}"` : ""; if (href.startsWith("#")) { return `${text}`; } return `${text}`; } override image({ href, text, title }: Marked.Tokens.Image) { return `${text ?? `; } override code({ lang: info, text }: Marked.Tokens.Code): string { // format: tsx // format: tsx my/file.ts // format: tsx "This is my title" let lang = ""; let title = ""; const match = info?.match(/^([\w_-]+)\s*(.*)?$/); if (match) { lang = match[1].toLocaleLowerCase(); title = match[2] ?? ""; } // Find icon by filename first, then by markdown block language. const icon = LOGOS.find((l) => l.file.test(title)) ?? LOGOS.find((l) => l.lang.test(lang)); let out = `
    `; if (title || icon) { const image = icon ? `${icon.text}` : ""; out += `
    ${image} ${title ? escapeHtml(String(title)) : " "} ${ icon && icon.text !== "Text" ? `` : "" }
    `; } const grammar = lang && Object.hasOwnProperty.call(Prism.languages, lang) ? Prism.languages[lang] : undefined; if (grammar === undefined) { out += `
    ${escapeHtml(text)}
    `; } else { const html = Prism.highlight(text, grammar, lang); out += `
    ${html}
    `; } out += `
    `; return out; } override blockquote({ text, tokens }: Marked.Tokens.Blockquote): string { const match = text.match(ADMISSION_REG); if (match) { const label: Record = { tip: "Tip", warn: "Warning", info: "Info", }; Marked.walkTokens(tokens, (token) => { if (token.type === "text" && token.text.startsWith(match[0])) { token.text = token.text.slice(match[0].length); } }); const type = match[1]; const icon = ``; return `
    \n${icon}${ label[type] }${this.parser.parse(tokens)}
    \n`; } return `
    \n${this.parser.parse(tokens)}
    \n`; } #assert(expr: unknown, msg: string): asserts expr { if (!expr) throw new Error(msg); } } export interface MarkdownOptions { inline?: boolean; } export function renderMarkdown( input: string, opts: MarkdownOptions = {}, ): { headings: MarkdownHeading[]; html: string } { const renderer = new DefaultRenderer(); const markedOpts: Marked.MarkedOptions & { async: false } = { gfm: true, async: false, renderer, }; try { const html = opts.inline ? Marked.parseInline(input, markedOpts) : Marked.parse(input, markedOpts); return { headings: renderer.headings, html }; } catch (err) { throw new HttpError(500, "Markdown parsing error", { cause: err, }); } } ================================================ FILE: www/utils/prism.ts ================================================ import Prism from "prismjs"; import "prismjs/components/prism-jsx.js"; import "prismjs/components/prism-typescript.js"; import "prismjs/components/prism-tsx.js"; import "prismjs/components/prism-diff.js"; import "prismjs/components/prism-json.js"; import "prismjs/components/prism-bash.js"; import "prismjs/components/prism-yaml.js"; import "prismjs/components/prism-ignore.js"; // deno-lint-ignore no-explicit-any const languages = Prism.languages as any; /** Extends `sh` with `deno` as a function token in Shell/Bash languages */ languages.sh.deno = { pattern: /(^|[\s;|&]|[<>]\()(?:deno)(?=$|[)\s;|&])/, lookbehind: true, alias: "function", }; languages.bash.deno = languages.sh.deno; /** * Adds `txt-files` language for file-structure code blocks. * * - Comments: Makes `#` and everything after a comment * - Operator: Matches the file structure symbols like `├──`, `└──`, and `│ ` * - Root: Matches `` or `` to indicate the root of the file structure * - Remaining text is rendered as plain text * * @example * ```txt-files * * ├── routes/ # File system based routes * │ ├── _app.tsx # Renders the outer content structure * │ └── index.tsx # Renders / * ├── dev.ts # Run this during development * └── main.ts # Run this for production * ``` */ Prism.languages["txt-files"] = { comment: { pattern: /#.*|$/, greedy: true, }, operator: /├──|└──|│\s+ /, root: { pattern: /|/, alias: "function", }, }; export { Prism }; ================================================ FILE: www/utils/screenshot.ts ================================================ import { launch } from "@astral/astral"; import { Image } from "imagescript"; export function validateArgs(args: string[]): [string, string] { if (args.length !== 2) { throw new Error("Usage: screenshot "); } return [args[0], args[1]]; } export function validateUrl(url: string): URL { const parsedUrl = new URL(url); if (parsedUrl.protocol !== "http:" && parsedUrl.protocol !== "https:") { throw new Error("Invalid URL"); } return parsedUrl; } export function generateFilePaths( id: string, ): { image2x: string; image1x: string } { return { image2x: `./www/static/showcase/${id}2x.jpg`, image1x: `./www/static/showcase/${id}1x.jpg`, }; } export async function captureScreenshot( url: string, id: string, ): Promise { const browser = await launch(); try { const page = await browser.newPage(url); await page.waitForNetworkIdle(); const raw = await page.screenshot(); const image2x = await Image.decode(raw); const { image2x: path2x, image1x: path1x } = generateFilePaths(id); await Deno.writeFile(path2x, await image2x.encodeJPEG(80)); const image1x = image2x.resize(image2x.width / 2, Image.RESIZE_AUTO); await Deno.writeFile(path1x, await image1x.encodeJPEG(80)); } finally { await browser.close(); } } // only run the script if it's the main module if (import.meta.main) { const [url, id] = validateArgs(Deno.args); validateUrl(url); await captureScreenshot(url, id); // deno-lint-ignore no-console console.log(`Screenshot saved as ${id}1x.jpg and ${id}2x.jpg`); } ================================================ FILE: www/utils/screenshot_test.ts ================================================ import { expect } from "@std/expect/expect"; import { generateFilePaths, validateArgs, validateUrl } from "./screenshot.ts"; Deno.test("validateArgs - accepts 2 arguments", () => { const result = validateArgs(["https://example.com", "test-id"]); expect(result).toEqual(["https://example.com", "test-id"]); }); Deno.test("validateArgs - should throw error for incorrect number of arguments", () => { expect(() => validateArgs(["only-one"])).toThrow( "Usage: screenshot ", ); expect(() => validateArgs(["one", "two", "three"])).toThrow( "Usage: screenshot ", ); }); Deno.test("validateUrl - should accept valid HTTP URLs", () => { const url = validateUrl("http://example.com"); expect(url).toEqual(new URL("http://example.com")); }); Deno.test("validateUrl - should accept valid HTTPS URLs", () => { const url = validateUrl("https://example.com"); expect(url).toEqual(new URL("https://example.com")); }); Deno.test("validateUrl - should reject invalid protocols", () => { expect(() => validateUrl("ftp://example.com")).toThrow("Invalid URL"); expect(() => validateUrl("file:///path/to/file")).toThrow("Invalid URL"); }); Deno.test("generateFilePaths - should generate correct file paths", () => { const paths = generateFilePaths("test-id"); expect(paths).toEqual({ image2x: "./www/static/showcase/test-id2x.jpg", image1x: "./www/static/showcase/test-id1x.jpg", }); }); Deno.test("generateFilePaths - should handle special characters in ID", () => { const paths = generateFilePaths("test-id_123"); expect(paths).toEqual({ image2x: "./www/static/showcase/test-id_1232x.jpg", image1x: "./www/static/showcase/test-id_1231x.jpg", }); }); ================================================ FILE: www/utils/state.ts ================================================ import { createDefine } from "fresh"; export interface State { title?: string; description?: string; ogImage?: string; noIndex?: boolean; } export const define = createDefine(); ================================================ FILE: www/vite.config.ts ================================================ import { defineConfig, type Plugin } from "vite"; import { fresh } from "@fresh/plugin-vite"; import tailwindcss from "@tailwindcss/vite"; import inspect from "vite-plugin-inspect"; import { copy } from "@std/fs"; import * as path from "@std/path"; import { pathWithRoot } from "../packages/plugin-vite/src/utils.ts"; export default defineConfig({ plugins: [ fresh(), tailwindcss(), inspect(), copyDocs(), ], }); function copyDocs(): Plugin { let serverOutDir = ""; return { name: "fresh:copy-docs", configResolved(config) { serverOutDir = pathWithRoot( config.environments.ssr.build.outDir, config.root, ); }, async writeBundle() { const branches = ["canary", "latest", "1.x"]; for (const branch of branches) { const source = path.join(import.meta.dirname!, "..", "docs", branch); const target = path.join(serverOutDir, "docs", branch); try { await Deno.remove(target, { recursive: true }); } catch (err) { if (!(err instanceof Deno.errors.NotFound)) { throw err; } } try { await Deno.mkdir(path.dirname(target), { recursive: true }); } catch { // Ignore } await copy(source, target); } }, }; }