diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 355d986144f..a9c8b627465 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -44,20 +44,7 @@ jobs: - name: Spell-check if: startsWith(matrix.os, 'ubuntu') && matrix.deno == 'v1.x' uses: crate-ci/typos@master - - - name: Cache dependencies and Chrome - uses: actions/cache@v4 - with: - path: | - ${{ matrix.cache_path }}deps - ${{ matrix.cache_path }}deno_puppeteer - key: ${{ runner.os }}-${{ hashFiles('**/*deps.ts', 'tests/fixture_twind_hydrate/twind.config.ts') }} - - - name: Install Chromium - run: deno run -A --unstable https://deno.land/x/puppeteer@16.2.0/install.ts - env: - PUPPETEER_PRODUCT: chrome - + - name: Type check project run: deno task check:types diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 00000000000..e88e74f34d3 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,36 @@ +name: Publish JSR + +on: + push: + branches: + - fresh-2.0-merge + +jobs: + publish: + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write + + steps: + - uses: actions/checkout@v4 + + - name: Install Deno + uses: denoland/setup-deno@v1 + with: + deno-version: v1.x + + - name: Publish Fresh + run: deno publish + + - name: Publish @fresh/init + working-directory: ./init + run: deno publish + + - name: Publish @fresh/plugin-tailwindcss + working-directory: ./plugin-tailwindcss + run: deno publish + + - name: Publish @fresh/update + working-directory: ./update + run: deno publish diff --git a/.gitignore b/.gitignore index e3856e793ed..43123f17746 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ _fresh/ tmp/ vendor/ node_modules/ +.docs/ .DS_Store diff --git a/.vscode/import_map.json b/.vscode/import_map.json deleted file mode 100644 index 5a999301b8c..00000000000 --- a/.vscode/import_map.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "scopes": { - "THIS FILE EXISTS ONLY FOR VSCODE! IT IS NOT USED AT RUNTIME": {} - }, - "imports": { - "$fresh/": "../", - "twind": "https://esm.sh/twind@0.16.19", - "twind/": "https://esm.sh/twind@0.16.19/", - "preact": "https://esm.sh/preact@10.22.0", - "preact/": "https://esm.sh/preact@10.22.0/", - "@preact/signals": "https://esm.sh/*@preact/signals@1.2.2", - "@preact/signals-core": "https://esm.sh/@preact/signals-core@1.5.1", - "@preact/signals-core@1.2.3": "https://esm.sh/@preact/signals-core@1.2.3", - "@preact/signals-core@1.3.0": "https://esm.sh/@preact/signals-core@1.3.0", - "$prism": "https://esm.sh/prismjs@1.29.0", - "$prism/": "https://esm.sh/prismjs@1.29.0/", - "$std/": "https://deno.land/std@0.216.0/", - "$ga4": "https://raw.githubusercontent.com/denoland/ga4/main/mod.ts", - "$marked-mangle": "https://esm.sh/marked-mangle@1.0.1", - "tailwindcss": "npm:tailwindcss@3.4.1", - "tailwindcss/": "npm:/tailwindcss@3.4.1/", - "tailwindcss/plugin": "npm:/tailwindcss@3.4.1/plugin.js" - } -} diff --git a/.vscode/settings.json b/.vscode/settings.json index ac698eaf0f8..2f0e1fa6dd1 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,7 +1,6 @@ { "deno.enable": true, "deno.lint": true, - "deno.importMap": "./.vscode/import_map.json", "deno.codeLens.test": true, "deno.documentPreloadLimit": 2000, "editor.formatOnSave": true, @@ -21,5 +20,10 @@ "[markdown]": { "editor.defaultFormatter": "denoland.vscode-deno" }, - "css.customData": [".vscode/tailwind.json"] + "css.customData": [ + ".vscode/tailwind.json" + ], + "[json]": { + "editor.defaultFormatter": "denoland.vscode-deno" + } } diff --git a/_typos.toml b/_typos.toml index 36c7512abad..f1be79c5c25 100644 --- a/_typos.toml +++ b/_typos.toml @@ -3,6 +3,7 @@ extend-exclude = [ "tests/fixture_partials/routes/scroll_restoration/index.tsx", "www/static/fonts/FixelVariable.woff2", "www/static/fonts/FixelVariableItalic.woff2", + "tests/lorem_ipsum.txt", ] [default] diff --git a/deno.json b/deno.json index 859483eb6ab..ed31a3eb2ee 100644 --- a/deno.json +++ b/deno.json @@ -1,24 +1,83 @@ { + "name": "@fresh/core", + "version": "2.0.0-alpha.9", + "exports": { + ".": "./src/mod.ts", + "./runtime": "./src/runtime/shared.ts", + "./client": "./src/runtime/client/mod.tsx", + "./client-dev": "./src/runtime/client/dev.ts", + "./dev": "./src/dev/mod.ts" + }, "lock": false, "tasks": { - "test": "deno test -A --parallel --trace-ops", + "test": "deno test -A --parallel src/ init/ update/ && deno test -A tests/ www/main_test.ts", "fixture": "deno run -A --watch=static/,routes/ tests/fixture/dev.ts", "www": "deno task --cwd=www start", + "build-www": "deno task --cwd=www build", "screenshot": "deno run -A www/utils/screenshot.ts", - "check:types": "deno check **/*.ts && deno check **/*.tsx", + "check:types": "deno check src/**/*.ts src/**/*.tsx tests/**/*.ts tests/**/*.tsx", "ok": "deno fmt --check && deno lint && deno task check:types && deno task test", - "install-puppeteer": "PUPPETEER_PRODUCT=chrome deno run -A --unstable https://deno.land/x/puppeteer@16.2.0/install.ts && PUPPETEER_PRODUCT=firefox deno run -A --unstable https://deno.land/x/puppeteer@16.2.0/install.ts", "test:www": "deno test -A tests/www/", "manifests": "deno run -A genAllManifest.ts" }, - "exclude": [ - "**/_fresh/*", - "**/tmp/*" - ], - "importMap": "./.vscode/import_map.json", + "exclude": ["**/_fresh/*", "**/tmp/*", "*/tests_OLD/**"], + "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"] + }, + "imports": { + "$ga4": "https://raw.githubusercontent.com/denoland/ga4/main/mod.ts", + "$marked-mangle": "https://esm.sh/marked-mangle@1.0.1", + "$std/": "https://deno.land/std@0.216.0/", + "@astral/astral": "jsr:@astral/astral@^0.4.0", + "@fresh/core": "./src/mod.ts", + "@fresh/core/client": "./src/runtime/client/mod.tsx", + "@fresh/core/client-dev": "./src/runtime/client/dev.ts", + "@fresh/core/dev": "./src/dev/mod.ts", + "@fresh/core/runtime": "./src/runtime/shared.ts", + "@fresh/plugin-tailwind": "./plugin-tailwindcss/src/mod.ts", + "@luca/esbuild-deno-loader": "jsr:@luca/esbuild-deno-loader@^0.10.3", + "@preact/signals": "npm:@preact/signals@^1.2.3", + "@std/async": "jsr:@std/async@^0.224.1", + "@std/cli": "jsr:@std/cli@^0.221.0", + "@std/crypto": "jsr:@std/crypto@^0.221.0", + "@std/datetime": "jsr:@std/datetime@^0.224.0", + "@std/encoding": "jsr:@std/encoding@^0.221.0", + "@std/expect": "jsr:@std/expect@^0.224.0", + "@std/fmt": "jsr:@std/fmt@^0.224.0", + "@std/fs": "jsr:@std/fs@^0.221.0", + "@std/html": "jsr:@std/html@^0.224.0", + "@std/jsonc": "jsr:@std/jsonc@^0.221.0", + "@std/media-types": "jsr:@std/media-types@^0.221.0", + "@std/path": "jsr:@std/path@^0.221.0", + "@std/semver": "jsr:@std/semver@^0.223.0", + "@std/streams": "jsr:@std/streams@^0.221.0", + "autoprefixer": "npm:autoprefixer@10.4.17", + "cssnano": "npm:cssnano@6.0.3", + "esbuild": "npm:esbuild@0.20.2", + "esbuild-wasm": "npm:esbuild-wasm@0.20.2", + "linkedom": "npm:linkedom@^0.16.11", + "postcss": "npm:postcss@8.4.35", + "preact": "npm:preact@^10.22.0", + "preact-render-to-string": "npm:preact-render-to-string@^6.4.2", + "tailwindcss": "npm:tailwindcss@^3.4.1", + "tailwindcss/plugin": "npm:/tailwindcss@^3.4.1/plugin.js", + "ts-morph": "npm:ts-morph@^22.0.0", + "twind": "https://esm.sh/twind@0.16.19", + "twind/": "https://esm.sh/twind@0.16.19/" + }, "compilerOptions": { - "jsx": "react-jsx", - "jsxImportSource": "preact" + "jsx": "precompile", + "jsxImportSource": "preact", + "jsxPrecompileSkipElements": ["a", "img", "source", "body", "html", "head"] }, "lint": { "rules": { "exclude": ["no-window"] } diff --git a/dev.ts b/dev.ts deleted file mode 100644 index 1e775bc0f10..00000000000 --- a/dev.ts +++ /dev/null @@ -1,2 +0,0 @@ -import { dev } from "./src/dev/dev_command.ts"; -export default dev; diff --git a/genAllManifest.ts b/genAllManifest.ts deleted file mode 100644 index 713f4285af8..00000000000 --- a/genAllManifest.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { walk } from "./src/dev/deps.ts"; -import { manifest } from "./src/dev/mod.ts"; -import type { FreshConfig } from "./src/server/mod.ts"; - -const skippedFixtures: string[] = [ - "fixture_invalid_handlers", - "fixture_update_check", -]; - -async function runGenerateInFixtures() { - for await (const entry of walk(Deno.cwd(), { maxDepth: 10 })) { - if (entry.isDirectory && entry.name.startsWith("fixture")) { - if (skippedFixtures.includes(entry.name)) { - console.log(`Skipping ${entry.path}\n`); - continue; - } - console.log(`Processing ${entry.path}`); - - try { - const configPath = `${entry.path}/fresh.config.ts`; - - let config: FreshConfig; - try { - config = (await import(configPath)).default; - } catch { - console.warn( - `No fresh.config.ts found or error in reading it at ${configPath}, using empty config.`, - ); - config = {}; - } - - await manifest(entry.path, config.router?.ignoreFilePattern); - console.log(`Manifest generated successfully in ${entry.path}\n`); - } catch (error) { - console.error(`Failed to process ${entry.path}:`, error); - console.log(); - } - } - } -} - -runGenerateInFixtures(); diff --git a/init.ts b/init.ts deleted file mode 100644 index caa65de9f38..00000000000 --- a/init.ts +++ /dev/null @@ -1,822 +0,0 @@ -import { basename, colors, join, parse, resolve } from "./src/dev/deps.ts"; -import { error } from "./src/dev/error.ts"; -import { collect, ensureMinDenoVersion, generate } from "./src/dev/mod.ts"; -import { - dotenvImports, - freshImports, - tailwindImports, - twindImports, -} from "./src/dev/imports.ts"; - -ensureMinDenoVersion(); - -const help = `fresh-init - -Initialize a new Fresh project. This will create all the necessary files for a -new project. - -To generate a project in the './foobar' subdirectory: - fresh-init ./foobar - -To generate a project in the current directory: - fresh-init . - -USAGE: - fresh-init [DIRECTORY] - -OPTIONS: - --force Overwrite existing files - --tailwind Use Tailwind for styling - --twind Use Twind for styling - --vscode Setup project for VS Code - --docker Setup Project to use Docker -`; - -const CONFIRM_EMPTY_MESSAGE = - "The target directory is not empty (files could get overwritten). Do you want to continue anyway?"; - -const USE_VSCODE_MESSAGE = "Do you use VS Code?"; - -const flags = parse(Deno.args, { - boolean: ["force", "tailwind", "twind", "vscode", "docker", "help"], - default: { - force: null, - tailwind: null, - twind: null, - vscode: null, - docker: null, - }, - alias: { - help: "h", - }, -}); - -if (flags.help) { - console.log(help); - Deno.exit(0); -} - -if (flags.tailwind && flags.twind) { - error("Cannot use Tailwind and Twind at the same time."); -} - -console.log(); -console.log( - colors.bgRgb8( - colors.rgb8(" 🍋 Fresh: The next-gen web framework. ", 0), - 121, - ), -); -console.log(); - -let unresolvedDirectory = Deno.args[0]; -if (flags._.length !== 1) { - const userInput = prompt("Project Name:", "fresh-project"); - if (!userInput) { - error(help); - } - - unresolvedDirectory = userInput; -} - -const resolvedDirectory = resolve(unresolvedDirectory); - -try { - const dir = [...Deno.readDirSync(resolvedDirectory)]; - const isEmpty = dir.length === 0 || - dir.length === 1 && dir[0].name === ".git"; - if ( - !isEmpty && - !(flags.force === null ? confirm(CONFIRM_EMPTY_MESSAGE) : flags.force) - ) { - error("Directory is not empty."); - } -} catch (err) { - if (!(err instanceof Deno.errors.NotFound)) { - throw err; - } -} -console.log("%cLet's set up your new Fresh project.\n", "font-weight: bold"); - -let useTailwind = flags.tailwind || false; -let useTwind = flags.twind || false; - -if (flags.tailwind == null && flags.twind == null) { - if (confirm("Do you want to use a styling library?")) { - console.log(); - console.log(`1. ${colors.cyan("tailwindcss")} (recommended)`); - console.log(`2. ${colors.cyan("Twind")}`); - console.log(); - switch ( - (prompt("Which styling library do you want to use? [1]") || "1").trim() - ) { - case "2": - useTwind = true; - break; - default: - useTailwind = true; - } - } -} - -const useVSCode = flags.vscode === null - ? confirm(USE_VSCODE_MESSAGE) - : flags.vscode; - -const useDocker = flags.docker; - -await Promise.all([ - Deno.mkdir(join(resolvedDirectory, "routes", "api"), { recursive: true }), - Deno.mkdir(join(resolvedDirectory, "islands"), { recursive: true }), - Deno.mkdir(join(resolvedDirectory, "static"), { recursive: true }), - Deno.mkdir(join(resolvedDirectory, "components"), { recursive: true }), -]); -if (useVSCode) { - await Deno.mkdir(join(resolvedDirectory, ".vscode"), { recursive: true }); -} - -const GITIGNORE = `# dotenv environment variable files -.env -.env.development.local -.env.test.local -.env.production.local -.env.local - -# Fresh build directory -_fresh/ -# npm dependencies -node_modules/ -`; - -await Deno.writeTextFile( - join(resolvedDirectory, ".gitignore"), - GITIGNORE, -); - -if (useDocker) { - const DENO_VERSION = Deno.version.deno; - const DOCKERFILE_TEXT = ` -FROM denoland/deno:${DENO_VERSION} - -ARG GIT_REVISION -ENV DENO_DEPLOYMENT_ID=\${GIT_REVISION} - -WORKDIR /app - -COPY . . -RUN deno cache main.ts - -EXPOSE 8000 - -CMD ["run", "-A", "main.ts"] - -`; - - await Deno.writeTextFile( - join(resolvedDirectory, "Dockerfile"), - DOCKERFILE_TEXT, - ); -} - -const ROUTES_INDEX_TSX = `import { useSignal } from "@preact/signals"; -import Counter from "../islands/Counter.tsx"; - -export default function Home() { - const count = useSignal(3); - return ( -
-
- the Fresh logo: a sliced lemon dripping with juice -

Welcome to Fresh

-

- Try updating this message in the - ./routes/index.tsx file, and refresh. -

- -
-
- ); -} -`; - -const COMPONENTS_BUTTON_TSX = `import { JSX } from "preact"; -import { IS_BROWSER } from "$fresh/runtime.ts"; - -export function Button(props: JSX.HTMLAttributes) { - return ( - -

{props.count}

- - - ); -} -`; - -// 404 page -const ROUTES_404_PAGE = `import { Head } from "$fresh/runtime.ts"; - -export default function Error404() { - return ( - <> - - 404 - Page not found - -
-
- the Fresh logo: a sliced lemon dripping with juice -

404 - Page not found

-

- The page you were looking for doesn't exist. -

- Go back home -
-
- - ); -} -`; -await Promise.all([ - Deno.writeTextFile( - join(resolvedDirectory, "routes", "index.tsx"), - ROUTES_INDEX_TSX, - ), - Deno.writeTextFile( - join(resolvedDirectory, "components", "Button.tsx"), - COMPONENTS_BUTTON_TSX, - ), - Deno.writeTextFile( - join(resolvedDirectory, "islands", "Counter.tsx"), - ISLANDS_COUNTER_TSX, - ), - Deno.writeTextFile( - join(resolvedDirectory, "routes", "_404.tsx"), - ROUTES_404_PAGE, - ), -]); - -const ROUTES_GREET_TSX = `import { PageProps } from "$fresh/server.ts"; - -export default function Greet(props: PageProps) { - return
Hello {props.params.name}
; -} -`; -await Deno.mkdir(join(resolvedDirectory, "routes", "greet"), { - recursive: true, -}); -await Deno.writeTextFile( - join(resolvedDirectory, "routes", "greet", "[name].tsx"), - ROUTES_GREET_TSX, -); - -const ROUTES_API_JOKE_TS = `import { FreshContext } from "$fresh/server.ts"; - -// Jokes courtesy of https://punsandoneliners.com/randomness/programmer-jokes/ -const JOKES = [ - "Why do Java developers often wear glasses? They can't C#.", - "A SQL query walks into a bar, goes up to two tables and says “can I join you?”", - "Wasn't hard to crack Forrest Gump's password. 1forrest1.", - "I love pressing the F5 key. It's refreshing.", - "Called IT support and a chap from Australia came to fix my network connection. I asked “Do you come from a LAN down under?”", - "There are 10 types of people in the world. Those who understand binary and those who don't.", - "Why are assembly programmers often wet? They work below C level.", - "My favourite computer based band is the Black IPs.", - "What programme do you use to predict the music tastes of former US presidential candidates? An Al Gore Rhythm.", - "An SEO expert walked into a bar, pub, inn, tavern, hostelry, public house.", -]; - -export const handler = (_req: Request, _ctx: FreshContext): Response => { - const randomIndex = Math.floor(Math.random() * JOKES.length); - const body = JOKES[randomIndex]; - return new Response(body); -}; -`; -await Deno.writeTextFile( - join(resolvedDirectory, "routes", "api", "joke.ts"), - ROUTES_API_JOKE_TS, -); - -const TAILWIND_CONFIG_TS = `import { type Config } from "tailwindcss"; - -export default { - content: [ - "{routes,islands,components}/**/*.{ts,tsx,js,jsx}", - ], -} satisfies Config; -`; -if (useTailwind) { - await Deno.writeTextFile( - join(resolvedDirectory, "tailwind.config.ts"), - TAILWIND_CONFIG_TS, - ); -} - -const TWIND_CONFIG_TS = `import { defineConfig, Preset } from "@twind/core"; -import presetTailwind from "@twind/preset-tailwind"; -import presetAutoprefix from "@twind/preset-autoprefix"; - -export default { - ...defineConfig({ - presets: [presetTailwind() as Preset, presetAutoprefix() as Preset], - }), - selfURL: import.meta.url, -}; -`; -if (useTwind) { - await Deno.writeTextFile( - join(resolvedDirectory, "twind.config.ts"), - TWIND_CONFIG_TS, - ); -} - -const NO_TAILWIND_STYLES = ` -*, -*::before, -*::after { - box-sizing: border-box; -} -* { - margin: 0; -} -button { - color: inherit; -} -button, [role="button"] { - cursor: pointer; -} -code { - font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, - "Liberation Mono", "Courier New", monospace; - font-size: 1em; -} -img, -svg { - display: block; -} -img, -video { - max-width: 100%; - height: auto; -} - -html { - line-height: 1.5; - -webkit-text-size-adjust: 100%; - font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, - "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, - "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; -} -.transition-colors { - transition-property: background-color, border-color, color, fill, stroke; - transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); - transition-duration: 150ms; -} -.my-6 { - margin-bottom: 1.5rem; - margin-top: 1.5rem; -} -.text-4xl { - font-size: 2.25rem; - line-height: 2.5rem; -} -.mx-2 { - margin-left: 0.5rem; - margin-right: 0.5rem; -} -.my-4 { - margin-bottom: 1rem; - margin-top: 1rem; -} -.mx-auto { - margin-left: auto; - margin-right: auto; -} -.px-4 { - padding-left: 1rem; - padding-right: 1rem; -} -.py-8 { - padding-bottom: 2rem; - padding-top: 2rem; -} -.bg-\\[\\#86efac\\] { - background-color: #86efac; -} -.text-3xl { - font-size: 1.875rem; - line-height: 2.25rem; -} -.py-6 { - padding-bottom: 1.5rem; - padding-top: 1.5rem; -} -.px-2 { - padding-left: 0.5rem; - padding-right: 0.5rem; -} -.py-1 { - padding-bottom: 0.25rem; - padding-top: 0.25rem; -} -.border-gray-500 { - border-color: #6b7280; -} -.bg-white { - background-color: #fff; -} -.flex { - display: flex; -} -.gap-8 { - grid-gap: 2rem; - gap: 2rem; -} -.font-bold { - font-weight: 700; -} -.max-w-screen-md { - max-width: 768px; -} -.flex-col { - flex-direction: column; -} -.items-center { - align-items: center; -} -.justify-center { - justify-content: center; -} -.border-2 { - border-width: 2px; -} -.rounded { - border-radius: 0.25rem; -} -.hover\\:bg-gray-200:hover { - background-color: #e5e7eb; -} -.tabular-nums { - font-variant-numeric: tabular-nums; -} -`; - -const APP_WRAPPER = `import { type PageProps } from "$fresh/server.ts"; -export default function App({ Component }: PageProps) { - return ( - - - - - ${basename(resolvedDirectory)} - ${useTwind ? "" : ``} - - - - - - ); -} -`; - -await Deno.writeTextFile( - join(resolvedDirectory, "routes", "_app.tsx"), - APP_WRAPPER, -); - -const TAILWIND_CSS = `@tailwind base; -@tailwind components; -@tailwind utilities;`; - -const cssStyles = useTailwind ? TAILWIND_CSS : NO_TAILWIND_STYLES; -if (!useTwind) { - await Deno.writeTextFile( - join(resolvedDirectory, "static", "styles.css"), - cssStyles, - ); -} - -const STATIC_LOGO = - ` - - - - -`; - -await Deno.writeTextFile( - join(resolvedDirectory, "static", "logo.svg"), - STATIC_LOGO, -); - -try { - const faviconArrayBuffer = await fetch("https://fresh.deno.dev/favicon.ico") - .then((d) => d.arrayBuffer()); - await Deno.writeFile( - join(resolvedDirectory, "static", "favicon.ico"), - new Uint8Array(faviconArrayBuffer), - ); -} catch { - // Skip this and be silent if there is a network issue. -} - -let FRESH_CONFIG_TS = `import { defineConfig } from "$fresh/server.ts";\n`; -if (useTailwind) { - FRESH_CONFIG_TS += `import tailwind from "$fresh/plugins/tailwind.ts"; -`; -} -if (useTwind) { - FRESH_CONFIG_TS += `import twind from "$fresh/plugins/twindv1.ts"; -import twindConfig from "./twind.config.ts"; -`; -} - -FRESH_CONFIG_TS += ` -export default defineConfig({${ - useTailwind - ? `\n plugins: [tailwind()],\n` - : useTwind - ? `\n plugins: [twind(twindConfig)],\n` - : "" -}}); -`; -const CONFIG_TS_PATH = join(resolvedDirectory, "fresh.config.ts"); -await Deno.writeTextFile(CONFIG_TS_PATH, FRESH_CONFIG_TS); - -let MAIN_TS = `/// -/// -/// -/// -/// - -import "$std/dotenv/load.ts"; - -import { start } from "$fresh/server.ts"; -import manifest from "./fresh.gen.ts"; -import config from "./fresh.config.ts"; -`; - -MAIN_TS += ` -await start(manifest, config);\n`; -const MAIN_TS_PATH = join(resolvedDirectory, "main.ts"); -await Deno.writeTextFile(MAIN_TS_PATH, MAIN_TS); - -const DEV_TS = `#!/usr/bin/env -S deno run -A --watch=static/,routes/ - -import dev from "$fresh/dev.ts"; -import config from "./fresh.config.ts"; - -import "$std/dotenv/load.ts"; - -await dev(import.meta.url, "./main.ts", config); -`; -const DEV_TS_PATH = join(resolvedDirectory, "dev.ts"); -await Deno.writeTextFile(DEV_TS_PATH, DEV_TS); -try { - await Deno.chmod(DEV_TS_PATH, 0o777); -} catch { - // this throws on windows -} - -const config = { - 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 .", - }, - lint: { - rules: { - tags: ["fresh", "recommended"], - }, - }, - exclude: ["**/_fresh/*"], - imports: {} as Record, - compilerOptions: { - jsx: "react-jsx", - jsxImportSource: "preact", - }, -}; -freshImports(config.imports); -if (useTailwind) { - tailwindImports(config.imports); - // Tailwind editor plugin expects the `node_modules` directory - // to be present, otherwise intellisense doesn't work. - // TODO: Have a better deno config type - // deno-lint-ignore no-explicit-any - (config as any).nodeModulesDir = true; -} -if (useTwind) { - twindImports(config.imports); -} -dotenvImports(config.imports); - -const DENO_CONFIG = JSON.stringify(config, null, 2) + "\n"; - -await Deno.writeTextFile(join(resolvedDirectory, "deno.json"), DENO_CONFIG); - -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://deno.land/manual/getting_started/installation - -Then start the project: - -\`\`\` -deno task start -\`\`\` - -This will watch the project directory and restart as necessary. -`; -await Deno.writeTextFile( - join(resolvedDirectory, "README.md"), - README_MD, -); - -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", - }, - "css.customData": useTailwind ? [".vscode/tailwind.json"] : undefined, -}; - -const VSCODE_SETTINGS = JSON.stringify(vscodeSettings, null, 2) + "\n"; - -if (useVSCode) { - await Deno.writeTextFile( - join(resolvedDirectory, ".vscode", "settings.json"), - VSCODE_SETTINGS, - ); -} - -const vscodeExtensions = { - recommendations: ["denoland.vscode-deno"], -}; - -if (useTailwind) { - vscodeExtensions.recommendations.push("bradlc.vscode-tailwindcss"); -} - -const VSCODE_EXTENSIONS = JSON.stringify(vscodeExtensions, null, 2) + "\n"; - -if (useVSCode) { - await Deno.writeTextFile( - join(resolvedDirectory, ".vscode", "extensions.json"), - VSCODE_EXTENSIONS, - ); -} - -const tailwindCustomData = { - "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", - }, - ], - }, - ], -}; -const TAILWIND_CUSTOMDATA = JSON.stringify(tailwindCustomData, null, 2) + "\n"; - -if (useVSCode && useTailwind) { - await Deno.writeTextFile( - join(resolvedDirectory, ".vscode", "tailwind.json"), - TAILWIND_CUSTOMDATA, - ); -} - -const manifest = await collect(resolvedDirectory); -await generate(resolvedDirectory, manifest); - -// 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 start%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", -); diff --git a/init/README.md b/init/README.md new file mode 100644 index 00000000000..eec288dd023 --- /dev/null +++ b/init/README.md @@ -0,0 +1,10 @@ +# Create a new Fresh project. + +This is a CLI tool to bootstrap a new Fresh project. To do so, run this command: + +```sh +deno run -Ar jsr:@fresh/init +``` + +Go to [https://fresh.deno.dev/](https://fresh.deno.dev/) for more information +about Fresh. diff --git a/init/deno.json b/init/deno.json new file mode 100644 index 00000000000..3083932771b --- /dev/null +++ b/init/deno.json @@ -0,0 +1,22 @@ +{ + "name": "@fresh/init", + "version": "0.0.1-alpha.7", + "exports": { + ".": "./src/mod.ts" + }, + "lock": false, + "exclude": ["**/tmp/*"], + "publish": { + "include": [ + "src/**/*.ts", + "deno.json", + "README.md" + ], + "exclude": ["**/*_test.*", "*.todo"] + }, + "imports": { + "@std/cli": "jsr:@std/cli@^0.221.0", + "@std/fmt": "jsr:@std/fmt@^0.221.0", + "@std/path": "jsr:@std/path@^0.221.0" + } +} diff --git a/init/src/init.ts b/init/src/init.ts new file mode 100644 index 00000000000..a1cb1adb13c --- /dev/null +++ b/init/src/init.ts @@ -0,0 +1,705 @@ +import * as colors from "@std/fmt/colors"; +import * as path from "@std/path"; + +export const enum InitStep { + ProjectName = "ProjectName", + Force = "Force", + Tailwind = "Tailwind", + VSCode = "VSCode", + Docker = "Docker", +} + +export class InitError extends Error {} + +function error(tty: MockTTY, message: string): never { + tty.logError(`%cerror%c: ${message}`, "color: red; font-weight: bold", ""); + throw new InitError(); +} + +export const HELP_TEXT = `@fresh/init + +Initialize a new Fresh project. This will create all the necessary files for a +new project. + +To generate a project in the './foobar' subdirectory: + deno run -Ar jsr:@fresh/init ./foobar + +To generate a project in the current directory: + deno run -Ar jsr:@fresh/init . + +USAGE: + deno run -Ar jsr:@fresh/init [DIRECTORY] + +OPTIONS: + --force Overwrite existing files + --tailwind Use Tailwind for styling + --vscode Setup project for VS Code + --docker Setup Project to use Docker +`; + +export interface MockTTY { + prompt( + step: InitStep, + message?: string | undefined, + _default?: string | undefined, + ): string | null; + confirm(step: InitStep, message?: string | undefined): boolean; + log(...args: unknown[]): void; + logError(...args: unknown[]): void; +} + +const realTTY: MockTTY = { + prompt(_step, message, _default) { + return prompt(message, _default); + }, + confirm(_step, message) { + return confirm(message); + }, + log(...args) { + console.log(...args); + }, + logError(...args) { + console.error(...args); + }, +}; + +export async function initProject( + cwd = Deno.cwd(), + input: (string | number)[], + flags: { + docker?: boolean | null; + force?: boolean | null; + tailwind?: boolean | null; + vscode?: boolean | null; + } = {}, + tty: MockTTY = realTTY, +): Promise { + tty.log(); + tty.log( + colors.bgRgb8( + colors.rgb8(" 🍋 Fresh: The next-gen web framework. ", 0), + 121, + ), + ); + tty.log(); + + let unresolvedDirectory = Deno.args[0]; + if (input.length !== 1) { + const userInput = tty.prompt( + InitStep.ProjectName, + "Project Name:", + "fresh-project", + ); + if (!userInput) { + error(tty, HELP_TEXT); + } + + unresolvedDirectory = userInput; + } + + const CONFIRM_EMPTY_MESSAGE = + "The target directory is not empty (files could get overwritten). Do you want to continue anyway?"; + + const projectDir = path.resolve(cwd, unresolvedDirectory); + + try { + const dir = [...Deno.readDirSync(projectDir)]; + const isEmpty = dir.length === 0 || + dir.length === 1 && dir[0].name === ".git"; + if ( + !isEmpty && + !(flags.force === null + ? tty.confirm(InitStep.Force, CONFIRM_EMPTY_MESSAGE) + : flags.force) + ) { + error(tty, "Directory is not empty."); + } + } catch (err) { + if (!(err instanceof Deno.errors.NotFound)) { + throw err; + } + } + + const useDocker = flags.docker; + let useTailwind = flags.tailwind || false; + if (flags.tailwind == null) { + if ( + tty.confirm( + InitStep.Tailwind, + `Set up ${colors.cyan("Tailwind CSS")} for styling?`, + ) + ) { + useTailwind = true; + } + } + + const USE_VSCODE_MESSAGE = `Do you use ${colors.cyan("VS Code")}?`; + const useVSCode = flags.vscode == null + ? tty.confirm(InitStep.VSCode, USE_VSCODE_MESSAGE) + : flags.vscode; + + const writeFile = async ( + pathname: string, + content: + | string + | Uint8Array + | ReadableStream + | Record, + ) => await writeProjectFile(projectDir, pathname, content); + + const GITIGNORE = `# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# Fresh build directory +_fresh/ +# npm dependencies +node_modules/ +`; + + await writeFile(".gitignore", GITIGNORE); + + if (useDocker) { + const DENO_VERSION = Deno.version.deno; + const DOCKERFILE_TEXT = ` +FROM denoland/deno:${DENO_VERSION} + +ARG GIT_REVISION +ENV DENO_DEPLOYMENT_ID=\${GIT_REVISION} + +WORKDIR /app + +COPY . . +RUN deno cache main.tsx + +EXPOSE 8000 + +CMD ["run", "-A", "main.tsx"] + +`; + await writeFile("Dockerfile", DOCKERFILE_TEXT); + } + + const TAILWIND_CONFIG_TS = `import { type Config } from "tailwindcss"; + +export default { + content: [ + "{routes,islands,components}/**/*.{ts,tsx}", + ], +} satisfies Config; +`; + if (useTailwind) { + await writeFile("tailwind.config.ts", TAILWIND_CONFIG_TS); + } + + const NO_TAILWIND_STYLES = ` +*, +*::before, +*::after { + box-sizing: border-box; +} +* { + margin: 0; +} +button { + color: inherit; +} +button, [role="button"] { + cursor: pointer; +} +code { + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, + "Liberation Mono", "Courier New", monospace; + font-size: 1em; +} +img, +svg { + display: block; +} +img, +video { + max-width: 100%; + height: auto; +} + +html { + line-height: 1.5; + -webkit-text-size-adjust: 100%; + font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, + "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, + "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; +} +.transition-colors { + transition-property: background-color, border-color, color, fill, stroke; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 150ms; +} +.my-6 { + margin-bottom: 1.5rem; + margin-top: 1.5rem; +} +.text-4xl { + font-size: 2.25rem; + line-height: 2.5rem; +} +.mx-2 { + margin-left: 0.5rem; + margin-right: 0.5rem; +} +.my-4 { + margin-bottom: 1rem; + margin-top: 1rem; +} +.mx-auto { + margin-left: auto; + margin-right: auto; +} +.px-4 { + padding-left: 1rem; + padding-right: 1rem; +} +.py-8 { + padding-bottom: 2rem; + padding-top: 2rem; +} +.bg-\\[\\#86efac\\] { + background-color: #86efac; +} +.text-3xl { + font-size: 1.875rem; + line-height: 2.25rem; +} +.py-6 { + padding-bottom: 1.5rem; + padding-top: 1.5rem; +} +.px-2 { + padding-left: 0.5rem; + padding-right: 0.5rem; +} +.py-1 { + padding-bottom: 0.25rem; + padding-top: 0.25rem; +} +.border-gray-500 { + border-color: #6b7280; +} +.bg-white { + background-color: #fff; +} +.flex { + display: flex; +} +.gap-8 { + grid-gap: 2rem; + gap: 2rem; +} +.font-bold { + font-weight: 700; +} +.max-w-screen-md { + max-width: 768px; +} +.flex-col { + flex-direction: column; +} +.items-center { + align-items: center; +} +.justify-center { + justify-content: center; +} +.border-2 { + border-width: 2px; +} +.rounded { + border-radius: 0.25rem; +} +.hover\\:bg-gray-200:hover { + background-color: #e5e7eb; +} +.tabular-nums { + font-variant-numeric: tabular-nums; +} +`; + + const TAILWIND_CSS = `@tailwind base; +@tailwind components; +@tailwind utilities;`; + + const cssStyles = useTailwind ? TAILWIND_CSS : NO_TAILWIND_STYLES; + await writeFile("static/styles.css", cssStyles); + + const STATIC_LOGO = + ` + + + + +`; + await writeFile("static/logo.svg", STATIC_LOGO); + + try { + const res = await fetch("https://fresh.deno.dev/favicon.ico"); + const buf = await res.arrayBuffer(); + await writeFile("static/favicon.ico", new Uint8Array(buf)); + } catch { + // Skip this and be silent if there is a network issue. + } + + const MAIN_TSX = `import { App, staticFiles, fsRoutes } from "@fresh/core"; +import { State } from "./utils.ts"; + +export const app = new App() + .use(staticFiles()) + .get("/api/:joke", () => new Response("Hello World")) + .get("/greet/:name", (ctx) => { + return ctx.render(

Hello {ctx.params.name}

); + }); + +await fsRoutes(app, { + loadIsland: (path) => import(\`./islands/\${path}\`), + loadRoute: (path) => import(\`./routes/\${path}\`), +}); + +if (import.meta.main) { + await app.listen(); +} +`; + await writeFile("main.tsx", MAIN_TSX); + + const COMPONENTS_BUTTON_TSX = `import { ComponentChildren } from "preact"; + +export interface ButtonProps { + 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/core/dev"; +import { app } from "./main.tsx"; + +const builder = new Builder(); +${useTailwind ? "tailwind(builder, app, {});\n" : "\n"} + +if (Deno.args.includes("build")) { + await builder.build(app); +} else { + await builder.listen(app); +} +`; + await writeFile("dev.ts", DEV_TS); + + const denoJson = { + tasks: { + check: + "deno fmt --check && deno lint && deno check **/*.ts && deno check **/*.tsx", + dev: "deno run -A --watch=static/,routes/ dev.ts", + build: "deno run -A dev.ts build", + start: "deno run -A main.ts", + update: "deno run -A -r jsr:@fresh/update .", + }, + lint: { + rules: { + tags: ["fresh", "recommended"], + }, + }, + exclude: ["**/_fresh/*"], + imports: { + "@fresh/core": "jsr:@fresh/core@^2.0.0-alpha.8", + "@fresh/plugin-tailwind": "jsr:@fresh/plugin-tailwind@^0.0.1-alpha.6", + "preact": "npm:preact@^10.22.0", + "@preact/signals": "npm:@preact/signals@^1.2.3", + } as Record, + compilerOptions: { + jsx: "react-jsx", + jsxImportSource: "preact", + }, + }; + + if (useTailwind) { + denoJson.imports["tailwindcss"] = "npm:tailwindcss@3.4.3"; + denoJson.imports["tailwindcss/plugin"] = "npm:tailwindcss@3.4.3/plugin.js"; + } + + await writeFile("deno.json", denoJson); + + 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://deno.land/manual/getting_started/installation + +Then start the project: + +\`\`\` +deno task start +\`\`\` + +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", + }, + "css.customData": useTailwind ? [".vscode/tailwind.json"] : 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 (useTailwind) { + const tailwindCustomData = { + "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", + }, + ], + }, + ], + }; + + await writeFile(".vscode/tailwind.json", tailwindCustomData); + } + } + + // Specifically print unresolvedDirectory, rather than resolvedDirectory in order to + // not leak personal info (e.g. `/Users/MyName`) + tty.log("\n%cProject initialized!\n", "color: green; font-weight: bold"); + + if (unresolvedDirectory !== ".") { + tty.log( + `Enter your project directory using %ccd ${unresolvedDirectory}%c.`, + "color: cyan", + "", + ); + } + tty.log( + "Run %cdeno task start%c to start the project. %cCTRL-C%c to stop.", + "color: cyan", + "", + "color: cyan", + "", + ); + tty.log(); + tty.log( + "Stuck? Join our Discord %chttps://discord.gg/deno", + "color: cyan", + "", + ); + tty.log(); + tty.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; + } + } +} diff --git a/init/src/init_test.ts b/init/src/init_test.ts new file mode 100644 index 00000000000..da61f54c28c --- /dev/null +++ b/init/src/init_test.ts @@ -0,0 +1,163 @@ +import { expect } from "@std/expect"; +import { initProject, InitStep, type MockTTY } from "./init.ts"; +import * as path from "@std/path"; +import { withBrowser } from "../../tests/test_utils.tsx"; +import { waitForText } from "../../tests/test_utils.tsx"; +import { withChildProcessServer } from "../../tests/test_utils.tsx"; + +async function withTmpDir(fn: (dir: string) => void | Promise) { + const dir = await Deno.makeTempDir(); + + try { + await fn(dir); + } finally { + await Deno.remove(dir, { recursive: true }); + } +} + +function mockUserInput(steps: Record) { + const errorOutput: unknown[][] = []; + const tty: MockTTY = { + confirm(step, _msg) { + return Boolean(steps[step]); + }, + prompt(step, _msg, def) { + const setting = typeof steps[step] === "string" + ? steps[step] as string + : null; + return setting ?? def ?? null; + }, + log: () => {}, + logError: (...args) => { + errorOutput.push(args); + }, + }; + return { + errorOutput, + tty, + }; +} + +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 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 - new project", async () => { + await withTmpDir(async (dir) => { + const mock = mockUserInput({}); + await initProject(dir, [], {}, mock.tty); + }); +}); + +Deno.test("init - create project dir", async () => { + await withTmpDir(async (dir) => { + const mock = mockUserInput({ [InitStep.ProjectName]: "fresh-init" }); + await initProject(dir, [], {}, mock.tty); + + const root = path.join(dir, "fresh-init"); + await expectProjectFile(root, "deno.json"); + await expectProjectFile(root, "main.tsx"); + await expectProjectFile(root, "dev.ts"); + await expectProjectFile(root, ".gitignore"); + await expectProjectFile(root, "static/styles.css"); + }); +}); + +Deno.test("init - with tailwind", async () => { + await withTmpDir(async (dir) => { + const mock = mockUserInput({ + [InitStep.ProjectName]: ".", + [InitStep.Tailwind]: true, + }); + await initProject(dir, [], {}, mock.tty); + + const css = await readProjectFile(dir, "static/styles.css"); + expect(css).toMatch(/@tailwind/); + + const main = await readProjectFile(dir, "main.tsx"); + const dev = await readProjectFile(dir, "dev.ts"); + expect(main).not.toMatch(/tailwind/); + expect(dev).toMatch(/tailwind/); + }); +}); + +Deno.test("init - with vscode", async () => { + await withTmpDir(async (dir) => { + const mock = mockUserInput({ + [InitStep.ProjectName]: ".", + [InitStep.VSCode]: true, + }); + await initProject(dir, [], {}, mock.tty); + + await expectProjectFile(dir, ".vscode/settings.json"); + await expectProjectFile(dir, ".vscode/extensions.json"); + }); +}); + +// TODO: Testing this with JSR isn't as easy anymore as it was before +Deno.test.ignore("init - can start dev server", async () => { + await withTmpDir(async (dir) => { + const mock = mockUserInput({ + [InitStep.ProjectName]: ".", + }); + await initProject(dir, [], {}, mock.tty); + await expectProjectFile(dir, "main.tsx"); + await expectProjectFile(dir, "dev.ts"); + + await withChildProcessServer( + dir, + path.join(dir, "dev.ts"), + async (address) => { + await withBrowser(async (page) => { + await page.goto(address); + await page.locator("button").click(); + await waitForText(page, "button + p", "2"); + }); + }, + ); + }); +}); + +// TODO: Testing this with JSR isn't as easy anymore as it was before +Deno.test.ignore("init - can start build project", async () => { + await withTmpDir(async (dir) => { + const mock = mockUserInput({ + [InitStep.ProjectName]: ".", + }); + await initProject(dir, [], {}, mock.tty); + await expectProjectFile(dir, "main.tsx"); + await expectProjectFile(dir, "dev.ts"); + + // Build + await new Deno.Command(Deno.execPath(), { + args: ["run", "-A", path.join(dir, "dev.ts"), "build"], + stdin: "null", + stdout: "piped", + stderr: "piped", + cwd: dir, + }).output(); + + await withChildProcessServer( + dir, + path.join(dir, "main.tsx"), + async (address) => { + console.log({ address }); + await withBrowser(async (page) => { + await page.goto(address); + await page.locator("button").click(); + await waitForText(page, "button + p", "2"); + }); + }, + ); + }); +}); diff --git a/init/src/mod.ts b/init/src/mod.ts new file mode 100644 index 00000000000..b9bc17a1fd1 --- /dev/null +++ b/init/src/mod.ts @@ -0,0 +1,26 @@ +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", "twind", "vscode", "docker", "help"], + default: { + force: null, + tailwind: null, + twind: null, + vscode: null, + docker: null, + }, + alias: { + help: "h", + }, +}); + +try { + await initProject(Deno.cwd(), flags._, flags); +} catch (err) { + if (err instanceof InitError) { + Deno.exit(1); + } + throw err; +} diff --git a/plugin-tailwindcss/README.md b/plugin-tailwindcss/README.md new file mode 100644 index 00000000000..1e72b669926 --- /dev/null +++ b/plugin-tailwindcss/README.md @@ -0,0 +1,29 @@ +# Tailwind CSS plugin for Fresh + +A Tailwind CSS plugin to use in Fresh. + +```ts +// dev.ts + +import { tailwind } from "@fresh/plugin-tailwind"; +import { FreshDevApp } from "@fresh/core/dev"; +import { app } from "./main.ts"; + +const devApp = new FreshDevApp(); + +// Enable Tailwind CSS +tailwind(devApp); + +devApp.mountApp("/", app); + +if (Deno.args.includes("build")) { + await devApp.build({ + target: "safari12", + }); +} else { + await devApp.listen(); +} +``` + +To learn more about Fresh go to +[https://fresh.deno.dev/](https://fresh.deno.dev/). diff --git a/plugin-tailwindcss/deno.json b/plugin-tailwindcss/deno.json new file mode 100644 index 00000000000..f1fd45536fe --- /dev/null +++ b/plugin-tailwindcss/deno.json @@ -0,0 +1,15 @@ +{ + "name": "@fresh/plugin-tailwind", + "version": "0.0.1-alpha.6", + "exports": { + ".": "./src/mod.ts" + }, + "imports": { + "@fresh/core": "jsr:@fresh/core@^2.0.0-alpha.1", + "@std/path": "jsr:@std/path@^0.221.0", + "autoprefixer": "npm:autoprefixer@10.4.17", + "cssnano": "npm:cssnano@6.0.3", + "postcss": "npm:postcss@8.4.35", + "tailwindcss": "npm:tailwindcss@^3.4.1" + } +} diff --git a/plugins/tailwind/compiler.ts b/plugin-tailwindcss/src/compiler.ts similarity index 80% rename from plugins/tailwind/compiler.ts rename to plugin-tailwindcss/src/compiler.ts index f1c08e1dd1d..76a4fb0fb0d 100644 --- a/plugins/tailwind/compiler.ts +++ b/plugin-tailwindcss/src/compiler.ts @@ -1,10 +1,10 @@ -import { ResolvedFreshConfig } from "../../server.ts"; -import tailwindCss, { Config } from "tailwindcss"; -import postcss from "npm:postcss@8.4.35"; -import cssnano from "npm:cssnano@6.0.3"; -import autoprefixer from "npm:autoprefixer@10.4.17"; -import * as path from "https://deno.land/std@0.216.0/path/mod.ts"; -import { TailwindPluginOptions } from "./types.ts"; +import tailwindCss, { type Config } from "tailwindcss"; +import postcss from "postcss"; +import cssnano from "cssnano"; +import autoprefixer from "autoprefixer"; +import * as path from "@std/path"; +import type { TailwindPluginOptions } from "./types.ts"; +import type { ResolvedFreshConfig } from "@fresh/core"; const CONFIG_EXTENSIONS = ["ts", "js", "mjs"]; @@ -51,7 +51,8 @@ export async function initTailwind( throw new Error(`Expected tailwind "content" option to be an array`); } - tailwindConfig.content = tailwindConfig.content.map((pattern) => { + // 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)); @@ -70,7 +71,7 @@ export async function initTailwind( autoprefixer(options.autoprefixer) as any, ]; - if (!config.dev) { + if (config.mode === "build") { plugins.push(cssnano()); } diff --git a/plugin-tailwindcss/src/mod.ts b/plugin-tailwindcss/src/mod.ts new file mode 100644 index 00000000000..05f37fefa6a --- /dev/null +++ b/plugin-tailwindcss/src/mod.ts @@ -0,0 +1,26 @@ +import type { TailwindPluginOptions } from "./types.ts"; +import { initTailwind } from "./compiler.ts"; +import type { Builder } from "@fresh/core/dev"; +import type { App } from "@fresh/core"; + +export function tailwind( + builder: Builder, + app: App, + options: TailwindPluginOptions = {}, +): void { + const processor = initTailwind(app.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(), + }; + }, + ); +} diff --git a/plugins/tailwind/types.ts b/plugin-tailwindcss/src/types.ts similarity index 100% rename from plugins/tailwind/types.ts rename to plugin-tailwindcss/src/types.ts diff --git a/plugins/tailwind.ts b/plugins/tailwind.ts deleted file mode 100644 index 8668bbace77..00000000000 --- a/plugins/tailwind.ts +++ /dev/null @@ -1,129 +0,0 @@ -import { Plugin, PluginMiddleware, ResolvedFreshConfig } from "../server.ts"; -import type postcss from "npm:postcss@8.4.35"; -import * as path from "https://deno.land/std@0.216.0/path/mod.ts"; -import { walk } from "https://deno.land/std@0.216.0/fs/walk.ts"; -import { TailwindPluginOptions } from "./tailwind/types.ts"; - -async function initTailwind( - config: ResolvedFreshConfig, - options: TailwindPluginOptions, -) { - return await (await import("./tailwind/compiler.ts")).initTailwind( - config, - options, - ); -} - -export default function tailwind( - options: TailwindPluginOptions = {}, -): Plugin { - let staticDir = path.join(Deno.cwd(), "static"); - let processor: postcss.Processor | null = null; - - const cache = new Map(); - - const tailwindMiddleware: PluginMiddleware = { - path: "/", - middleware: { - handler: async (_req, ctx) => { - const pathname = ctx.url.pathname; - - if (pathname.endsWith(".css.map")) { - const cached = cache.get(pathname); - if (cached) return Response.json(cached.map); - } - - if (!pathname.endsWith(".css") || !processor) { - return ctx.next(); - } - - let cached = cache.get(pathname); - if (!cached) { - const filePath = path.join( - staticDir, - pathname.replace(ctx.config.basePath, ""), - ); - let text = ""; - try { - text = await Deno.readTextFile(filePath); - const res = await processor.process(text, { - from: undefined, - }); - - cached = { - content: res.content, - map: res.map?.toString() ?? "", - }; - cache.set(pathname, cached); - } catch (err) { - // If the file is not found than it's likely a virtual file - // by the user that they respond to via a middleware. - if (err instanceof Deno.errors.NotFound) { - return ctx.next(); - } - - cached = { - content: text, - map: "", - }; - console.error(err); - } - } - - return new Response(cached!.content, { - status: 200, - headers: { - "Content-Type": "text/css", - "Cache-Control": "no-cache, no-store, max-age=0, must-revalidate", - }, - }); - }, - }, - }; - - const middlewares: Plugin["middlewares"] = []; - - return { - name: "tailwind", - async configResolved(config) { - if (config.dev) { - staticDir = config.staticDir; - processor = await initTailwind(config, options); - middlewares.push(tailwindMiddleware); - } - }, - middlewares, - async buildStart(config) { - staticDir = config.staticDir; - const outDir = path.join(config.build.outDir, "static"); - - processor = await initTailwind(config, options); - - const files = walk(config.staticDir, { - exts: ["css"], - includeDirs: false, - includeFiles: true, - }); - - for await (const file of files) { - const content = await Deno.readTextFile(file.path); - const result = await processor.process(content, { - from: undefined, - }); - - const relFilePath = path.relative(staticDir, file.path); - const outPath = path.join(outDir, relFilePath); - - try { - await Deno.mkdir(path.dirname(outPath), { recursive: true }); - } catch (err) { - if (!(err instanceof Deno.errors.AlreadyExists)) { - throw err; - } - } - - await Deno.writeTextFile(outPath, result.content); - } - }, - }; -} diff --git a/plugins/twind.ts b/plugins/twind.ts deleted file mode 100644 index 3c8c9d2d843..00000000000 --- a/plugins/twind.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { virtualSheet } from "twind/sheets"; -import { Plugin } from "../server.ts"; - -import { Options, setup, STYLE_ELEMENT_ID } from "./twind/shared.ts"; -export type { Options }; - -export default function twind(options: Options): Plugin { - const sheet = virtualSheet(); - setup(options, sheet); - const main = `data:application/javascript,import hydrate from "${ - new URL("./twind/main.ts", import.meta.url).href - }"; -import options from "${options.selfURL}"; -export default function(state) { hydrate(options, state); }`; - return { - name: "twind", - entrypoints: { "main": main }, - async renderAsync(ctx) { - sheet.reset(undefined); - await ctx.renderAsync(); - const cssTexts = [...sheet.target]; - const snapshot = sheet.reset(); - const precedences = snapshot[1] as number[]; - - const cssText = cssTexts.map((cssText, i) => - `${cssText}/*${precedences[i].toString(36)}*/` - ).join("\n"); - - const mappings: (string | [string, string])[] = []; - for ( - const [key, value] of (snapshot[3] as Map).entries() - ) { - if (key === value) { - mappings.push(key); - } else { - mappings.push([key, value]); - } - } - - return { - scripts: [{ entrypoint: "main", state: mappings }], - styles: [{ cssText, id: STYLE_ELEMENT_ID }], - }; - }, - }; -} diff --git a/plugins/twind/main.ts b/plugins/twind/main.ts deleted file mode 100644 index 8943564f073..00000000000 --- a/plugins/twind/main.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { Sheet } from "twind"; -import { Options, setup, STYLE_ELEMENT_ID } from "./shared.ts"; - -type State = [string, string][]; - -export default function hydrate(options: Options, state: State) { - const el = document.getElementById(STYLE_ELEMENT_ID) as HTMLStyleElement; - const rules = new Set(); - const precedences: number[] = []; - const mappings = new Map( - state.map((v) => typeof v === "string" ? [v, v] : v), - ); - // deno-lint-ignore no-explicit-any - const sheetState: any[] = [precedences, rules, mappings, true]; - const target = el.sheet!; - const ruleText = Array.from(target.cssRules).map((r) => r.cssText); - for (const r of ruleText) { - const m = r.lastIndexOf("/*"); - const precedence = parseInt(r.slice(m + 2, -2), 36); - const rule = r.slice(0, m); - rules.add(rule); - precedences.push(precedence); - } - const sheet: Sheet = { - target, - insert: (rule, index) => target.insertRule(rule, index), - init: (cb) => cb(sheetState.shift()), - }; - setup(options, sheet); -} diff --git a/plugins/twind/shared.ts b/plugins/twind/shared.ts deleted file mode 100644 index 4caa81d6a2a..00000000000 --- a/plugins/twind/shared.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { JSX, options as preactOptions, VNode } from "preact"; -import { Configuration, setup as twSetup, Sheet, tw } from "twind"; - -type PreactOptions = typeof preactOptions & { __b?: (vnode: VNode) => void }; - -export const STYLE_ELEMENT_ID = "__FRSH_TWIND"; - -export interface Options extends Omit { - /** The import.meta.url of the module defining these options. */ - selfURL: string; -} - -declare module "preact" { - namespace JSX { - interface DOMAttributes { - class?: string; - className?: string; - } - } -} - -export function setup(options: Options, sheet: Sheet) { - const config: Configuration = { - ...options, - mode: "silent", - sheet, - }; - twSetup(config); - - // Hook into options._diff which is called whenever a new comparison - // starts in Preact. - const originalHook = (preactOptions as PreactOptions).__b; - (preactOptions as PreactOptions).__b = ( - // deno-lint-ignore no-explicit-any - vnode: VNode>, - ) => { - if (typeof vnode.type === "string" && typeof vnode.props === "object") { - const { props } = vnode; - const classes: string[] = []; - if (props.class) { - classes.push(tw(props.class)); - props.class = undefined; - } - if (props.className) { - classes.push(tw(props.className)); - props.className = undefined; - } - if (classes.length) { - props.class = classes.join(" "); - } - } - - originalHook?.(vnode); - }; -} diff --git a/plugins/twindv1.ts b/plugins/twindv1.ts deleted file mode 100644 index 7f1efa5e724..00000000000 --- a/plugins/twindv1.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { stringify, virtual } from "$fresh/plugins/twindv1_deps.ts"; -import { Plugin } from "$fresh/server.ts"; - -import { - Options, - setup, - STYLE_ELEMENT_ID, -} from "$fresh/plugins/twindv1/shared.ts"; - -import { BaseTheme } from "$fresh/plugins/twindv1_deps.ts"; -export type { Options }; - -export default function twindv1( - options: Options, -): Plugin { - const sheet = virtual(true); - setup(options, sheet); - const main = `data:application/javascript,import hydrate from "${ - new URL("./twindv1/main.ts", import.meta.url).href - }"; -import options from "${options.selfURL}"; -export default function(state) { hydrate(options, state); }`; - return { - name: "twind", - entrypoints: { "main": main }, - async renderAsync(ctx) { - await ctx.renderAsync(); - const cssText = stringify(sheet.target); - return { - scripts: [{ entrypoint: "main", state: [] }], - styles: [{ cssText, id: STYLE_ELEMENT_ID }], - }; - }, - }; -} diff --git a/plugins/twindv1/main.ts b/plugins/twindv1/main.ts deleted file mode 100644 index f6f871a6421..00000000000 --- a/plugins/twindv1/main.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { cssom, getSheet, setup, TwindConfig } from "../twindv1_deps.ts"; -import { STYLE_ELEMENT_ID } from "./shared.ts"; - -export default function hydrate(options: TwindConfig) { - const elem = document.getElementById(STYLE_ELEMENT_ID) as HTMLStyleElement; - const sheet = cssom(elem); - - sheet.resume = getSheet().resume.bind(sheet); - document.querySelector('[data-twind="claimed"]')?.remove(); - - setup(options, sheet); -} diff --git a/plugins/twindv1/shared.ts b/plugins/twindv1/shared.ts deleted file mode 100644 index fb03888fe2d..00000000000 --- a/plugins/twindv1/shared.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { JSX, options as preactOptions, VNode } from "preact"; -import { - BaseTheme, - setup as twSetup, - Sheet, - tw, - TwindConfig, -} from "$fresh/plugins/twindv1_deps.ts"; - -type PreactOptions = typeof preactOptions & { __b?: (vnode: VNode) => void }; - -export const STYLE_ELEMENT_ID = "__FRSH_TWIND"; - -export interface Options - extends TwindConfig { - /** The import.meta.url of the module defining these options. */ - selfURL: string; -} - -declare module "preact" { - namespace JSX { - interface DOMAttributes { - class?: string; - className?: string; - } - } -} - -export function setup( - { selfURL: _selfURL, ...config }: Options, - sheet: Sheet, -) { - twSetup(config, sheet); - - // Hook into options._diff which is called whenever a new comparison - // starts in Preact. - const originalHook = (preactOptions as PreactOptions).__b; - (preactOptions as PreactOptions).__b = ( - // deno-lint-ignore no-explicit-any - vnode: VNode>, - ) => { - if (typeof vnode.type === "string" && typeof vnode.props === "object") { - const { props } = vnode; - const classes: string[] = []; - if (props.class) { - classes.push(tw(props.class)); - props.class = undefined; - } - if (props.className) { - classes.push(tw(props.className)); - props.className = undefined; - } - if (classes.length) { - props.class = classes.join(" "); - } - } - - originalHook?.(vnode); - }; -} diff --git a/plugins/twindv1_deps.ts b/plugins/twindv1_deps.ts deleted file mode 100644 index 163f4d421a3..00000000000 --- a/plugins/twindv1_deps.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "https://esm.sh/@twind/core@1.1.3"; diff --git a/runtime.ts b/runtime.ts deleted file mode 100644 index df42b5f9950..00000000000 --- a/runtime.ts +++ /dev/null @@ -1,5 +0,0 @@ -import "./src/types.ts"; -export * from "./src/runtime/utils.ts"; -export * from "./src/runtime/head.ts"; -export * from "./src/runtime/csp.ts"; -export * from "./src/runtime/Partial.tsx"; diff --git a/server.ts b/server.ts deleted file mode 100644 index e81fecdd210..00000000000 --- a/server.ts +++ /dev/null @@ -1,2 +0,0 @@ -import "./src/types.ts"; -export * from "./src/server/mod.ts"; diff --git a/src/app.ts b/src/app.ts new file mode 100644 index 00000000000..d2a7b4515fc --- /dev/null +++ b/src/app.ts @@ -0,0 +1,319 @@ +import { DENO_DEPLOYMENT_ID } from "./runtime/build_id.ts"; +import * as colors from "@std/fmt/colors"; +import { type MiddlewareFn, runMiddlewares } from "./middlewares/mod.ts"; +import { FreshReqContext } from "./context.ts"; +import { + mergePaths, + type Method, + type Router, + UrlPatternRouter, +} from "./router.ts"; +import { + type FreshConfig, + normalizeConfig, + type ResolvedFreshConfig, +} from "./config.ts"; +import { type BuildCache, ProdBuildCache } from "./build_cache.ts"; +import * as path from "@std/path"; +import { type ComponentType, h } from "preact"; +import type { ServerIslandRegistry } from "./context.ts"; +import { renderToString } from "preact-render-to-string"; +import { FinishSetup, ForgotBuild } from "./finish_setup.tsx"; +import { HttpError } from "./error.ts"; + +// TODO: Completed type clashes in older Deno versions +// deno-lint-ignore no-explicit-any +export const DEFAULT_CONN_INFO: any = { + localAddr: { transport: "tcp", hostname: "localhost", port: 8080 }, + remoteAddr: { transport: "tcp", hostname: "localhost", port: 1234 }, +}; + +const DEFAULT_NOT_FOUND = () => { + throw new HttpError(404); +}; +const DEFAULT_NOT_ALLOWED_METHOD = () => { + throw new HttpError(405); +}; + +export type ListenOptions = Partial & { + remoteAddress?: string; +}; + +export interface RouteCacheEntry { + params: Record; + handler: MiddlewareFn; +} + +export let getRouter: (app: App) => Router>; +// deno-lint-ignore no-explicit-any +export let getIslandRegistry: (app: App) => ServerIslandRegistry; +// deno-lint-ignore no-explicit-any +export let getBuildCache: (app: App) => BuildCache | null; +// deno-lint-ignore no-explicit-any +export let setBuildCache: (app: App, cache: BuildCache | null) => void; + +export class App { + #router: Router> = new UrlPatternRouter< + MiddlewareFn + >(); + #islandRegistry: ServerIslandRegistry = new Map(); + #buildCache: BuildCache | null = null; + #islandNames = new Set(); + + static { + getRouter = (app) => app.#router; + getIslandRegistry = (app) => app.#islandRegistry; + getBuildCache = (app) => app.#buildCache; + setBuildCache = (app, cache) => app.#buildCache = cache; + } + + /** + * The final resolved Fresh configuration. + */ + config: ResolvedFreshConfig; + + constructor(config: FreshConfig = {}) { + this.config = normalizeConfig(config); + } + + island( + filePathOrUrl: string | URL, + exportName: string, + // deno-lint-ignore no-explicit-any + fn: ComponentType, + ): this { + const filePath = filePathOrUrl instanceof URL + ? filePathOrUrl.href + : filePathOrUrl; + + // Create unique island name + let name = exportName === "default" + ? path.basename(filePath, path.extname(filePath)) + : exportName; + if (this.#islandNames.has(name)) { + let i = 0; + while (this.#islandNames.has(`${name}_${i}`)) { + i++; + } + name = `${name}_${i}`; + } + + this.#islandRegistry.set(fn, { fn, exportName, name, file: filePathOrUrl }); + return this; + } + + use(middleware: MiddlewareFn): this { + this.#router.addMiddleware(middleware); + return this; + } + + get(path: string, ...middlewares: MiddlewareFn[]): this { + return this.#addRoutes("GET", path, middlewares); + } + post(path: string, ...middlewares: MiddlewareFn[]): this { + return this.#addRoutes("POST", path, middlewares); + } + patch(path: string, ...middlewares: MiddlewareFn[]): this { + return this.#addRoutes("PATCH", path, middlewares); + } + put(path: string, ...middlewares: MiddlewareFn[]): this { + return this.#addRoutes("PUT", path, middlewares); + } + delete(path: string, ...middlewares: MiddlewareFn[]): this { + return this.#addRoutes("DELETE", path, middlewares); + } + head(path: string, ...middlewares: MiddlewareFn[]): this { + return this.#addRoutes("HEAD", path, middlewares); + } + all(path: string, ...middlewares: MiddlewareFn[]): this { + return this.#addRoutes("ALL", path, middlewares); + } + + mountApp(path: string, app: App): this { + const routes = app.#router._routes; + app.#islandRegistry.forEach((value, key) => { + this.#islandRegistry.set(key, value); + }); + + const middlewares = app.#router._middlewares; + + // Special case when user calls one of these: + // - `app.mounApp("/", otherApp)` + // - `app.mounApp("*", otherApp)` + const isSelf = path === "*" || path === "/"; + if (isSelf && middlewares.length > 0) { + this.#router._middlewares.push(...middlewares); + } + + for (let i = 0; i < routes.length; i++) { + const route = routes[i]; + + const merged = typeof route.path === "string" + ? mergePaths(path, route.path) + : route.path; + const combined = isSelf + ? route.handlers + : middlewares.concat(route.handlers); + this.#router.add(route.method, merged, combined); + } + + return this; + } + + #addRoutes( + method: Method | "ALL", + pathname: string | URLPattern, + middlewares: MiddlewareFn[], + ): this { + const merged = typeof pathname === "string" + ? mergePaths(this.config.basePath, pathname) + : pathname; + this.#router.add(method, merged, middlewares); + return this; + } + + async handler(): Promise< + (request: Request, info?: Deno.ServeHandlerInfo) => Promise + > { + if (this.#buildCache === null) { + this.#buildCache = await ProdBuildCache.fromSnapshot(this.config); + } + + if ( + !this.#buildCache.hasSnapshot && this.config.mode === "production" && + DENO_DEPLOYMENT_ID !== undefined + ) { + return missingBuildHandler; + } + + return async ( + req: Request, + conn: Deno.ServeHandlerInfo | Deno.ServeUnixHandlerInfo = + DEFAULT_CONN_INFO, + ) => { + const url = new URL(req.url); + // Prevent open redirect attacks + url.pathname = url.pathname.replace(/\/+/g, "/"); + + const method = req.method.toUpperCase() as Method; + const matched = this.#router.match(method, url); + + const next = matched.patternMatch && !matched.methodMatch + ? DEFAULT_NOT_ALLOWED_METHOD + : DEFAULT_NOT_FOUND; + + const { params, handlers } = matched; + const ctx = new FreshReqContext( + req, + this.config, + next, + this.#islandRegistry, + this.#buildCache!, + conn, + ); + + ctx.params = params; + + try { + if (handlers.length === 1 && handlers[0].length === 1) { + return handlers[0][0](ctx); + } + + ctx.next = next; + return await runMiddlewares(handlers, ctx); + } catch (err) { + if (err instanceof HttpError) { + if (err.status >= 500) { + console.error(err); + } + return new Response(err.message, { status: err.status }); + } + + console.error(err); + return new Response("Internal server error", { status: 500 }); + } + }; + } + + async listen(options: ListenOptions = {}): Promise { + if (!options.onListen) { + options.onListen = (params) => { + const pathname = (this.config.basePath) + "/"; + const protocol = options.key && options.cert ? "https:" : "http:"; + // Work around https://github.com/denoland/deno/issues/23650 + const hostname = params.hostname.startsWith("::") + ? `[${params.hostname}]` + : params.hostname; + const address = colors.cyan( + `${protocol}//${hostname}:${params.port}${pathname}`, + ); + const localLabel = colors.bold("Local:"); + + // Print more concise output for deploy logs + if (DENO_DEPLOYMENT_ID) { + console.log( + colors.bgRgb8(colors.rgb8(" 🍋 Fresh ready ", 0), 121), + `${localLabel} ${address}`, + ); + } else { + console.log(); + console.log( + colors.bgRgb8(colors.rgb8(" 🍋 Fresh ready ", 0), 121), + ); + const sep = options.remoteAddress ? "" : "\n"; + const space = options.remoteAddress ? " " : ""; + console.log(` ${localLabel} ${space}${address}${sep}`); + if (options.remoteAddress) { + const remoteLabel = colors.bold("Remote:"); + const remoteAddress = colors.cyan(options.remoteAddress); + console.log(` ${remoteLabel} ${remoteAddress}\n`); + } + } + }; + } + + const handler = await this.handler(); + if (options.port) { + await Deno.serve(options, handler); + } else { + // No port specified, check for a free port. Instead of picking just + // any port we'll check if the next one is free for UX reasons. + // That way the user only needs to increment a number when running + // multiple apps vs having to remember completely different ports. + let firstError; + for (let port = 8000; port < 8020; port++) { + try { + await Deno.serve({ ...options, port }, handler); + firstError = undefined; + break; + } catch (err) { + if (err instanceof Deno.errors.AddrInUse) { + // Throw first EADDRINUSE error + // if no port is free + if (!firstError) { + firstError = err; + } + continue; + } + + throw err; + } + } + + if (firstError) { + throw firstError; + } + } + } +} + +// deno-lint-ignore require-await +const missingBuildHandler = async (): Promise => { + const headers = new Headers(); + headers.set("Content-Type", "text/html; charset=utf-8"); + + const html = DENO_DEPLOYMENT_ID + ? renderToString(h(FinishSetup, null)) + : renderToString(h(ForgotBuild, null)); + return new Response(html, { headers, status: 500 }); +}; diff --git a/src/app_test.tsx b/src/app_test.tsx new file mode 100644 index 00000000000..b77da4e5ef8 --- /dev/null +++ b/src/app_test.tsx @@ -0,0 +1,472 @@ +import { expect } from "@std/expect"; +import { App, setBuildCache } from "./app.ts"; +import { FakeServer } from "./test_utils.ts"; +import { ProdBuildCache } from "./build_cache.ts"; + +Deno.test("FreshApp - .use()", async () => { + const app = new App<{ text: string }>() + .use((ctx) => { + ctx.state.text = "A"; + return ctx.next(); + }) + .use((ctx) => { + ctx.state.text += "B"; + return ctx.next(); + }) + .get("/", (ctx) => new Response(ctx.state.text)); + + const server = new FakeServer(await app.handler()); + + const res = await server.get("/"); + expect(await res.text()).toEqual("AB"); +}); + +Deno.test("FreshApp - .use() #2", async () => { + const app = new App<{ text: string }>() + .use(() => new Response("ok #1")) + .get("/foo/bar", () => new Response("ok #2")) + .get("/", () => new Response("ok #3")); + + const server = new FakeServer(await app.handler()); + + const res = await server.get("/"); + expect(await res.text()).toEqual("ok #1"); +}); + +Deno.test("FreshApp - .get()", async () => { + const app = new App() + .post("/", () => new Response("ok")) + .post("/foo", () => new Response("ok")) + .get("/", () => new Response("ok")) + .get("/foo", () => new Response("ok")); + + const server = new FakeServer(await app.handler()); + + let res = await server.get("/"); + expect(await res.text()).toEqual("ok"); + + res = await server.get("/foo"); + expect(await res.text()).toEqual("ok"); +}); + +Deno.test("FreshApp - .get() with basePath", async () => { + const app = new App({ basePath: "/foo/bar" }) + .get("/", () => new Response("ok")) + .get("/foo", () => new Response("ok")); + + const server = new FakeServer(await app.handler()); + + let res = await server.get("/"); + expect(res.status).toEqual(404); + res = await server.get("/foo"); + expect(res.status).toEqual(404); + + res = await server.get("/foo/bar"); + expect(res.status).toEqual(200); + res = await server.get("/foo/bar/foo"); + expect(res.status).toEqual(200); +}); + +Deno.test("FreshApp - .post()", async () => { + const app = new App<{ text: string }>() + .get("/", () => new Response("fail")) + .get("/foo", () => new Response("fail")) + .post("/", () => new Response("ok")) + .post("/foo", () => new Response("ok")); + + const server = new FakeServer(await app.handler()); + + let res = await server.post("/"); + expect(await res.text()).toEqual("ok"); + + res = await server.post("/foo"); + expect(await res.text()).toEqual("ok"); +}); + +Deno.test("FreshApp - .post() with basePath", async () => { + const app = new App({ basePath: "/foo/bar" }) + .post("/", () => new Response("ok")) + .post("/foo", () => new Response("ok")); + + const server = new FakeServer(await app.handler()); + + let res = await server.post("/"); + expect(res.status).toEqual(404); + res = await server.post("/foo"); + expect(res.status).toEqual(404); + + res = await server.post("/foo/bar"); + expect(res.status).toEqual(200); + res = await server.post("/foo/bar/foo"); + expect(res.status).toEqual(200); +}); + +Deno.test("FreshApp - .patch()", async () => { + const app = new App<{ text: string }>() + .get("/", () => new Response("fail")) + .get("/foo", () => new Response("fail")) + .patch("/", () => new Response("ok")) + .patch("/foo", () => new Response("ok")); + + const server = new FakeServer(await app.handler()); + + let res = await server.patch("/"); + expect(await res.text()).toEqual("ok"); + + res = await server.patch("/foo"); + expect(await res.text()).toEqual("ok"); +}); + +Deno.test("FreshApp - .patch() with basePath", async () => { + const app = new App({ basePath: "/foo/bar" }) + .patch("/", () => new Response("ok")) + .patch("/foo", () => new Response("ok")); + + const server = new FakeServer(await app.handler()); + + let res = await server.patch("/"); + expect(res.status).toEqual(404); + res = await server.patch("/foo"); + expect(res.status).toEqual(404); + + res = await server.patch("/foo/bar"); + expect(res.status).toEqual(200); + res = await server.patch("/foo/bar/foo"); + expect(res.status).toEqual(200); +}); + +Deno.test("FreshApp - .put()", async () => { + const app = new App<{ text: string }>() + .get("/", () => new Response("fail")) + .get("/foo", () => new Response("fail")) + .put("/", () => new Response("ok")) + .put("/foo", () => new Response("ok")); + + const server = new FakeServer(await app.handler()); + + let res = await server.put("/"); + expect(await res.text()).toEqual("ok"); + + res = await server.put("/foo"); + expect(await res.text()).toEqual("ok"); +}); + +Deno.test("FreshApp - .put() with basePath", async () => { + const app = new App({ basePath: "/foo/bar" }) + .put("/", () => new Response("ok")) + .put("/foo", () => new Response("ok")); + + const server = new FakeServer(await app.handler()); + + let res = await server.put("/"); + expect(res.status).toEqual(404); + res = await server.put("/foo"); + expect(res.status).toEqual(404); + + res = await server.put("/foo/bar"); + expect(res.status).toEqual(200); + res = await server.put("/foo/bar/foo"); + expect(res.status).toEqual(200); +}); + +Deno.test("FreshApp - .delete()", async () => { + const app = new App<{ text: string }>() + .get("/", () => new Response("fail")) + .get("/foo", () => new Response("fail")) + .delete("/", () => new Response("ok")) + .delete("/foo", () => new Response("ok")); + + const server = new FakeServer(await app.handler()); + + let res = await server.delete("/"); + expect(await res.text()).toEqual("ok"); + + res = await server.delete("/foo"); + expect(await res.text()).toEqual("ok"); +}); + +Deno.test("FreshApp - .delete() with basePath", async () => { + const app = new App({ basePath: "/foo/bar" }) + .delete("/", () => new Response("ok")) + .delete("/foo", () => new Response("ok")); + + const server = new FakeServer(await app.handler()); + + let res = await server.delete("/"); + expect(res.status).toEqual(404); + res = await server.delete("/foo"); + expect(res.status).toEqual(404); + + res = await server.delete("/foo/bar"); + expect(res.status).toEqual(200); + res = await server.delete("/foo/bar/foo"); + expect(res.status).toEqual(200); +}); + +Deno.test("FreshApp - wrong method match", async () => { + const app = new App<{ text: string }>() + .get("/", () => new Response("ok")) + .post("/", () => new Response("ok")); + + const server = new FakeServer(await app.handler()); + + let res = await server.put("/"); + expect(res.status).toEqual(405); + expect(await res.text()).toEqual("Method Not Allowed"); + + res = await server.post("/"); + expect(res.status).toEqual(200); + expect(await res.text()).toEqual("ok"); +}); + +Deno.test("FreshApp - methods with middleware", async () => { + const app = new App<{ text: string }>() + .use((ctx) => { + ctx.state.text = "A"; + return ctx.next(); + }) + .get("/", (ctx) => new Response(ctx.state.text)) + .post("/", (ctx) => new Response(ctx.state.text)); + + const server = new FakeServer(await app.handler()); + + let res = await server.get("/"); + expect(await res.text()).toEqual("A"); + + res = await server.post("/"); + expect(await res.text()).toEqual("A"); +}); + +Deno.test("FreshApp - .mountApp() compose apps", async () => { + const innerApp = new App<{ text: string }>() + .use((ctx) => { + ctx.state.text = "A"; + return ctx.next(); + }) + .get("/", (ctx) => new Response(ctx.state.text)) + .post("/", (ctx) => new Response(ctx.state.text)); + + const app = new App<{ text: string }>() + .get("/", () => new Response("ok")) + .mountApp("/foo", innerApp); + + const server = new FakeServer(await app.handler()); + + let res = await server.get("/"); + expect(await res.text()).toEqual("ok"); + + res = await server.get("/foo"); + expect(await res.text()).toEqual("A"); + + res = await server.post("/foo"); + expect(await res.text()).toEqual("A"); +}); + +Deno.test("FreshApp - .mountApp() self mount, no middleware", async () => { + const innerApp = new App<{ text: string }>() + .use((ctx) => { + ctx.state.text = "A"; + return ctx.next(); + }) + .get("/foo", (ctx) => new Response(ctx.state.text)) + .post("/foo", (ctx) => new Response(ctx.state.text)); + + const app = new App<{ text: string }>() + .get("/", () => new Response("ok")) + .mountApp("/", innerApp); + + const server = new FakeServer(await app.handler()); + + let res = await server.get("/"); + expect(await res.text()).toEqual("ok"); + + res = await server.get("/foo"); + expect(await res.text()).toEqual("A"); + + res = await server.post("/foo"); + expect(await res.text()).toEqual("A"); +}); + +Deno.test( + "FreshApp - .mountApp() self mount, with middleware", + async () => { + const innerApp = new App<{ text: string }>() + .use(function B(ctx) { + ctx.state.text += "B"; + return ctx.next(); + }) + .get("/foo", (ctx) => new Response(ctx.state.text)) + .post("/foo", (ctx) => new Response(ctx.state.text)); + + const app = new App<{ text: string }>() + .use(function A(ctx) { + ctx.state.text = "A"; + return ctx.next(); + }) + .get("/", () => new Response("ok")) + .mountApp("/", innerApp); + + const server = new FakeServer(await app.handler()); + + let res = await server.get("/"); + expect(await res.text()).toEqual("ok"); + + res = await server.get("/foo"); + expect(await res.text()).toEqual("AB"); + + res = await server.post("/foo"); + expect(await res.text()).toEqual("AB"); + }, +); + +Deno.test( + "FreshApp - .mountApp() self mount, different order", + async () => { + const innerApp = new App<{ text: string }>() + .get("/foo", (ctx) => new Response(ctx.state.text)) + .use(function B(ctx) { + ctx.state.text += "B"; + return ctx.next(); + }) + .post("/foo", (ctx) => new Response(ctx.state.text)); + + const app = new App<{ text: string }>() + .use(function A(ctx) { + ctx.state.text = "A"; + return ctx.next(); + }) + .get("/", () => new Response("ok")) + .mountApp("/", innerApp); + + const server = new FakeServer(await app.handler()); + + let res = await server.get("/"); + expect(await res.text()).toEqual("ok"); + + res = await server.get("/foo"); + expect(await res.text()).toEqual("AB"); + + res = await server.post("/foo"); + expect(await res.text()).toEqual("AB"); + }, +); + +Deno.test("FreshApp - .mountApp() self mount empty", async () => { + const innerApp = new App<{ text: string }>() + .use((ctx) => { + ctx.state.text = "A"; + return ctx.next(); + }) + .get("/foo", (ctx) => new Response(ctx.state.text)); + + const app = new App<{ text: string }>() + .mountApp("/", innerApp); + + const server = new FakeServer(await app.handler()); + + const res = await server.get("/foo"); + expect(await res.text()).toEqual("A"); +}); + +Deno.test( + "FreshApp - .mountApp() self mount with middleware", + async () => { + const innerApp = new App<{ text: string }>() + .use(function Inner(ctx) { + ctx.state.text += "_Inner"; + return ctx.next(); + }) + .get("/", (ctx) => new Response(ctx.state.text)); + + const app = new App<{ text: string }>() + .use(function Outer(ctx) { + ctx.state.text = "Outer"; + return ctx.next(); + }) + .mountApp("/", innerApp); + + const server = new FakeServer(await app.handler()); + + const res = await server.get("/"); + expect(await res.text()).toEqual("Outer_Inner"); + }, +); + +Deno.test("FreshApp - catches errors", async () => { + let thrownErr: unknown | null = null; + const app = new App<{ text: string }>() + .use(async (ctx) => { + ctx.state.text = "A"; + try { + return await ctx.next(); + } catch (err) { + thrownErr = err; + throw err; + } + }) + .get("/", () => { + throw new Error("fail"); + }); + + const server = new FakeServer(await app.handler()); + + const res = await server.get("/"); + expect(res.status).toEqual(500); + expect(thrownErr).toBeInstanceOf(Error); +}); + +// TODO: Find a better way to test this +Deno.test.ignore("FreshApp - finish setup", async () => { + const app = new App<{ text: string }>() + .get("/", (ctx) => { + return ctx.render(
ok
); + }); + + setBuildCache( + app, + await ProdBuildCache.fromSnapshot({ + ...app.config, + build: { + outDir: "foo", + }, + }), + ); + + const server = new FakeServer(await app.handler()); + const res = await server.get("/"); + const text = await res.text(); + expect(text).toContain("Finish setting up"); + expect(res.status).toEqual(500); +}); + +Deno.test("FreshApp - sets error on context", async () => { + const thrown: [unknown, unknown][] = []; + const app = new App() + .use(async (ctx) => { + try { + return await ctx.next(); + } catch (err) { + thrown.push([err, ctx.error]); + throw err; + } + }) + .use(async (ctx) => { + try { + return await ctx.next(); + } catch (err) { + thrown.push([err, ctx.error]); + throw err; + } + }) + .get("/", () => { + throw ""; + }); + + const server = new FakeServer(await app.handler()); + + const res = await server.get("/"); + await res.body?.cancel(); + expect(thrown.length).toEqual(2); + expect(thrown[0][0]).toEqual(thrown[0][1]); + expect(thrown[1][0]).toEqual(thrown[1][1]); +}); diff --git a/src/build/aot_snapshot.ts b/src/build/aot_snapshot.ts deleted file mode 100644 index a17d7fea8da..00000000000 --- a/src/build/aot_snapshot.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { ResolvedFreshConfig } from "../server/types.ts"; -import { colors, join } from "../server/deps.ts"; -import type { BuildSnapshot, BuildSnapshotJson } from "./mod.ts"; -import { setBuildId } from "../server/build_id.ts"; - -export class AotSnapshot implements BuildSnapshot { - #files: Map; - #dependencies: Map; - - constructor( - files: Map, - dependencies: Map, - ) { - this.#files = files; - this.#dependencies = dependencies; - } - - get paths(): string[] { - return Array.from(this.#files.keys()); - } - - async read(path: string): Promise | null> { - const filePath = this.#files.get(path); - if (filePath !== undefined) { - try { - const file = await Deno.open(filePath, { read: true }); - return file.readable; - } catch (_err) { - return null; - } - } - - // Handler will turn this into a 404 - return null; - } - - dependencies(path: string): string[] { - return this.#dependencies.get(path) ?? []; - } -} - -export async function loadAotSnapshot( - config: ResolvedFreshConfig, -): Promise { - const snapshotDirPath = config.build.outDir; - try { - if ((await Deno.stat(snapshotDirPath)).isDirectory) { - console.log( - `Using snapshot found at ${colors.cyan(snapshotDirPath)}`, - ); - - const snapshotPath = join(snapshotDirPath, "snapshot.json"); - const json = JSON.parse( - await Deno.readTextFile(snapshotPath), - ) as BuildSnapshotJson; - setBuildId(json.build_id); - - const dependencies = new Map( - Object.entries(json.files), - ); - - const files = new Map(); - Object.keys(json.files).forEach((name) => { - const filePath = join(snapshotDirPath, name); - files.set(name, filePath); - }); - - return new AotSnapshot(files, dependencies); - } - return null; - } catch (err) { - if (!(err instanceof Deno.errors.NotFound)) { - throw err; - } - return null; - } -} diff --git a/src/build/deps.ts b/src/build/deps.ts deleted file mode 100644 index a23100149da..00000000000 --- a/src/build/deps.ts +++ /dev/null @@ -1,10 +0,0 @@ -// -- $std -- -export { - fromFileUrl, - join, - relative, - toFileUrl, -} from "https://deno.land/std@0.216.0/path/mod.ts"; -export { escape as regexpEscape } from "https://deno.land/std@0.216.0/regexp/escape.ts"; -export { denoPlugins } from "jsr:@luca/esbuild-deno-loader@0.10.3"; -export { assertEquals } from "https://deno.land/std@0.216.0/assert/mod.ts"; diff --git a/src/build/esbuild.ts b/src/build/esbuild.ts deleted file mode 100644 index 51e36e4b978..00000000000 --- a/src/build/esbuild.ts +++ /dev/null @@ -1,217 +0,0 @@ -import { - type BuildOptions, - type OnLoadOptions, - type Plugin, -} from "https://deno.land/x/esbuild@v0.20.2/mod.js"; -import { denoPlugins, fromFileUrl, regexpEscape, relative } from "./deps.ts"; -import { Builder, BuildSnapshot } from "./mod.ts"; - -export interface EsbuildBuilderOptions { - /** The build ID. */ - buildID: string; - /** The entrypoints, mapped from name to URL. */ - entrypoints: Record; - /** Whether or not this is a dev build. */ - dev: boolean; - /** The path to the deno.json / deno.jsonc config file. */ - configPath: string; - /** The JSX configuration. */ - jsx?: string; - jsxImportSource?: string; - target: string | string[]; - absoluteWorkingDir: string; - basePath?: string; -} - -let esbuild: typeof import("https://deno.land/x/esbuild@v0.20.2/mod.js"); - -export async function initializeEsbuild() { - esbuild = - // deno-lint-ignore no-deprecated-deno-api - Deno.run === undefined || - Deno.env.get("FRESH_ESBUILD_LOADER") === "portable" - ? await import("https://deno.land/x/esbuild@v0.20.2/wasm.js") - : await import("https://deno.land/x/esbuild@v0.20.2/mod.js"); - const esbuildWasmURL = - new URL("./esbuild_v0.20.2.wasm", import.meta.url).href; - - // deno-lint-ignore no-deprecated-deno-api - if (Deno.run === undefined) { - await esbuild.initialize({ - wasmURL: esbuildWasmURL, - worker: false, - }); - } else { - await esbuild.initialize({}); - } - return esbuild; -} - -export class EsbuildBuilder implements Builder { - #options: EsbuildBuilderOptions; - - constructor(options: EsbuildBuilderOptions) { - this.#options = options; - } - - async build(): Promise { - const opts = this.#options; - - // Lazily initialize esbuild - const esbuild = await initializeEsbuild(); - - try { - const absWorkingDir = opts.absoluteWorkingDir; - - // In dev-mode we skip identifier minification to be able to show proper - // component names in Preact DevTools instead of single characters. - const minifyOptions: Partial = opts.dev - ? { - minifyIdentifiers: false, - minifySyntax: true, - minifyWhitespace: true, - } - : { minify: true }; - - const bundle = await esbuild.build({ - entryPoints: opts.entrypoints, - - platform: "browser", - target: this.#options.target, - - format: "esm", - bundle: true, - splitting: true, - treeShaking: true, - sourcemap: opts.dev ? "linked" : false, - ...minifyOptions, - - jsx: opts.jsx === "react" - ? "transform" - : opts.jsx === "react-native" || opts.jsx === "preserve" - ? "preserve" - : !opts.jsxImportSource - ? "transform" - : "automatic", - jsxImportSource: opts.jsxImportSource ?? "preact", - - absWorkingDir, - outdir: ".", - write: false, - metafile: true, - - plugins: [ - devClientUrlPlugin(opts.basePath), - buildIdPlugin(opts.buildID), - ...denoPlugins({ configPath: opts.configPath }), - ], - }); - - const files = new Map(); - const dependencies = new Map(); - - if (bundle.outputFiles) { - for (const file of bundle.outputFiles) { - const path = relative(absWorkingDir, file.path); - files.set(path, file.contents); - } - } - - files.set( - "metafile.json", - new TextEncoder().encode(JSON.stringify(bundle.metafile)), - ); - - if (bundle.metafile) { - const metaOutputs = new Map(Object.entries(bundle.metafile.outputs)); - - for (const [path, entry] of metaOutputs.entries()) { - const imports = entry.imports - .filter(({ kind }) => kind === "import-statement") - .map(({ path }) => path); - dependencies.set(path, imports); - } - } - - return new EsbuildSnapshot(files, dependencies); - } finally { - await esbuild.stop(); - } - } -} - -function devClientUrlPlugin(basePath?: string): Plugin { - return { - name: "dev-client-url", - setup(build) { - build.onLoad( - { filter: /client\.ts$/, namespace: "file" }, - async (args) => { - // Load the original script - const contents = await Deno.readTextFile(args.path); - - // Replace the URL - const modifiedContents = contents.replace( - "/_frsh/alive", - `${basePath}/_frsh/alive`, - ); - - return { - contents: modifiedContents, - loader: "ts", - }; - }, - ); - }, - }; -} - -function buildIdPlugin(buildId: string): Plugin { - const file = import.meta.resolve("../runtime/build_id.ts"); - const url = new URL(file); - let options: OnLoadOptions; - if (url.protocol === "file:") { - const path = fromFileUrl(url); - const filter = new RegExp(`^${regexpEscape(path)}$`); - options = { filter, namespace: "file" }; - } else { - const namespace = url.protocol.slice(0, -1); - const path = url.href.slice(namespace.length + 1); - const filter = new RegExp(`^${regexpEscape(path)}$`); - options = { filter, namespace }; - } - return { - name: "fresh-build-id", - setup(build) { - build.onLoad( - options, - () => ({ contents: `export const BUILD_ID = "${buildId}";` }), - ); - }, - }; -} - -export class EsbuildSnapshot implements BuildSnapshot { - #files: Map; - #dependencies: Map; - - constructor( - files: Map, - dependencies: Map, - ) { - this.#files = files; - this.#dependencies = dependencies; - } - - get paths(): string[] { - return Array.from(this.#files.keys()); - } - - read(path: string): Uint8Array | null { - return this.#files.get(path) ?? null; - } - - dependencies(path: string): string[] { - return this.#dependencies.get(path) ?? []; - } -} diff --git a/src/build/esbuild_test.ts b/src/build/esbuild_test.ts deleted file mode 100644 index 66420e0e25d..00000000000 --- a/src/build/esbuild_test.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { assertEquals } from "./deps.ts"; -import { fromFileUrl, join, toFileUrl } from "../server/deps.ts"; -import { EsbuildBuilder } from "./esbuild.ts"; - -const denoJson = join( - fromFileUrl(import.meta.url), - "..", - "..", - "..", - "deno.json", -); - -const mainEntry = toFileUrl(join( - fromFileUrl(import.meta.url), - "..", - "..", - "runtime", - "entrypoints", - "client.ts", -)).href; - -Deno.test("esbuild", async (t) => { - await t.step("esbuild snapshot with cwd=Deno.cwd()", async () => { - const builder = new EsbuildBuilder({ - absoluteWorkingDir: Deno.cwd(), - buildID: "foo", - configPath: denoJson, - dev: false, - entrypoints: { - main: mainEntry, - }, - jsx: "react-jsx", - target: "es2020", - }); - - const snapshot = await builder.build(); - assertEquals(snapshot.paths, ["main.js", "metafile.json"]); - }); - - await t.step({ - name: "esbuild snapshot with cwd=/", - ignore: Deno.build.os === "windows", - fn: async () => { - const builder = new EsbuildBuilder({ - absoluteWorkingDir: "/", - buildID: "foo", - configPath: denoJson, - dev: false, - entrypoints: { - main: mainEntry, - }, - jsx: "react-jsx", - target: "es2020", - }); - - const snapshot = await builder.build(); - assertEquals(snapshot.paths, ["main.js", "metafile.json"]); - }, - }); -}); diff --git a/src/build/esbuild_v0.20.2.wasm b/src/build/esbuild_v0.20.2.wasm deleted file mode 100644 index 8ff72c225b2..00000000000 Binary files a/src/build/esbuild_v0.20.2.wasm and /dev/null differ diff --git a/src/build/mod.ts b/src/build/mod.ts deleted file mode 100644 index 1013ddb5d2b..00000000000 --- a/src/build/mod.ts +++ /dev/null @@ -1,34 +0,0 @@ -export { - EsbuildBuilder, - type EsbuildBuilderOptions, - EsbuildSnapshot, -} from "./esbuild.ts"; -export { AotSnapshot } from "./aot_snapshot.ts"; -export interface Builder { - build(): Promise; -} - -export interface BuildSnapshot { - /** The list of files contained in this snapshot, not prefixed by a slash. */ - readonly paths: string[]; - - /** For a given file, return it's contents. - * @throws If the file is not contained in this snapshot. */ - read( - path: string, - ): - | ReadableStream - | Uint8Array - | null - | Promise | Uint8Array | null>; - - /** For a given entrypoint, return it's list of dependencies. - * - * Returns an empty array if the entrypoint does not exist. */ - dependencies(path: string): string[]; -} - -export interface BuildSnapshotJson { - build_id: string; - files: Record; -} diff --git a/src/build_cache.ts b/src/build_cache.ts new file mode 100644 index 00000000000..0ee1bf09ebe --- /dev/null +++ b/src/build_cache.ts @@ -0,0 +1,110 @@ +import * as path from "@std/path"; +import type { ResolvedFreshConfig } from "./config.ts"; +import { setBuildId } from "./runtime/build_id.ts"; + +export interface FileSnapshot { + generated: boolean; + hash: string | null; +} + +export interface BuildSnapshot { + version: number; + buildId: string; + staticFiles: Record; + islands: Record; +} + +export interface StaticFile { + hash: string | null; + size: number; + readable: ReadableStream | Uint8Array; +} + +export interface BuildCache { + hasSnapshot: boolean; + readFile(pathname: string): Promise; + getIslandChunkName(islandName: string): string | null; +} + +export class ProdBuildCache implements BuildCache { + static async fromSnapshot(config: ResolvedFreshConfig) { + const snapshotPath = path.join(config.build.outDir, "snapshot.json"); + + const staticFiles = new Map(); + const islandToChunk = new Map(); + + let hasSnapshot = false; + try { + const content = await Deno.readTextFile(snapshotPath); + const snapshot = JSON.parse(content) as BuildSnapshot; + hasSnapshot = true; + setBuildId(snapshot.buildId); + + const files = Object.keys(snapshot.staticFiles); + for (let i = 0; i < files.length; i++) { + const pathname = files[i]; + const info = snapshot.staticFiles[pathname]; + staticFiles.set(pathname, info); + } + + const islands = Object.keys(snapshot.islands); + for (let i = 0; i < islands.length; i++) { + const pathname = islands[i]; + islandToChunk.set(pathname, snapshot.islands[pathname]); + } + } catch (err) { + if (!(err instanceof Deno.errors.NotFound)) { + throw err; + } + } + + return new ProdBuildCache(config, islandToChunk, staticFiles, hasSnapshot); + } + + #islands: Map; + #fileInfo: Map; + #config: ResolvedFreshConfig; + + constructor( + config: ResolvedFreshConfig, + islands: Map, + files: Map, + public hasSnapshot: boolean, + ) { + this.#islands = islands; + this.#fileInfo = files; + this.#config = config; + } + + async readFile(pathname: string): Promise { + const info = this.#fileInfo.get(pathname); + if (info === undefined) return null; + + const base = info.generated + ? this.#config.build.outDir + : this.#config.staticDir; + const filePath = info.generated + ? path.join(base, "static", pathname) + : path.join(base, pathname); + + // Check if path resolves outside of intended directory. + if (path.relative(base, filePath).startsWith(".")) { + return null; + } + + const [stat, file] = await Promise.all([ + Deno.stat(filePath), + Deno.open(filePath), + ]); + + return { + hash: info.hash, + size: stat.size, + readable: file.readable, + }; + } + + getIslandChunkName(islandName: string): string | null { + return this.#islands.get(islandName) ?? null; + } +} diff --git a/src/compat/server.ts b/src/compat/server.ts new file mode 100644 index 00000000000..b0757ad1a42 --- /dev/null +++ b/src/compat/server.ts @@ -0,0 +1,59 @@ +import type { VNode } from "preact"; +import type { FreshContext } from "../context.ts"; +import type { HandlerByMethod } from "../handlers.ts"; + +/** + * @deprecated Use {@link FreshContext} instead + */ +export type AppProps = FreshContext; +/** + * @deprecated Use {@link FreshContext} instead + */ +export type LayoutProps = FreshContext; +/** + * @deprecated Use {@link FreshContext} instead + */ +export type UnknownPageProps = FreshContext< + Data, + T +>; +/** + * @deprecated Use {@link FreshContext} instead + */ +export type ErrorPageProps = FreshContext; + +/** + * @deprecated Use {@link FreshContext} instead + */ +// deno-lint-ignore no-explicit-any +export type RouteContext> = FreshContext< + T, + S +>; + +// deno-lint-ignore no-explicit-any +export type Handlers> = + HandlerByMethod; + +function defineFn( + fn: ( + ctx: FreshContext, + ) => Request | VNode | null | Promise, +): ( + ctx: FreshContext, +) => Request | VNode | null | Promise { + return fn; +} + +/** + * @deprecated Use {@link definePage} instead + */ +export const defineApp = defineFn; +/** + * @deprecated Use {@link definePage} instead + */ +export const defineRoute = defineFn; +/** + * @deprecated Use {@link definePage} instead + */ +export const defineLayout = defineFn; diff --git a/src/config.ts b/src/config.ts new file mode 100644 index 00000000000..399e00bad43 --- /dev/null +++ b/src/config.ts @@ -0,0 +1,74 @@ +import * as path from "@std/path"; +import { MODE, type Mode } from "./runtime/server/mod.tsx"; + +export interface FreshPlugin { + name: string; +} + +export interface FreshConfig { + root?: string; + build?: { + /** + * The directory to write generated files to when `dev.ts build` is run. + * This can be an absolute path, a file URL or a relative path. + */ + outDir?: string; + }; + /** + * Serve fresh from a base path instead of from the root. + * "/foo/bar" -> http://localhost:8000/foo/bar + * @default {undefined} + */ + basePath?: string; + staticDir?: string; +} + +/** + * The final resolved Fresh configuration where fields the user didn't specify are set to the default values. + */ +export interface ResolvedFreshConfig { + root: string; + build: { + outDir: string; + }; + basePath: string; + staticDir: string; + /** + * Tells you in which mode Fresh is currently running in. + */ + mode: Mode; +} + +export function parseRootPath(root: string): string { + if (root.startsWith("file://")) { + root = path.fromFileUrl(root); + } + + const ext = path.extname(root); + if ( + ext === ".ts" || ext === ".tsx" || ext === ".js" || ext === ".jsx" || + ext === ".mjs" + ) { + root = path.dirname(root); + } + + return root; +} + +export function normalizeConfig(options: FreshConfig): ResolvedFreshConfig { + const root = options.root ? parseRootPath(options.root) : Deno.cwd(); + + return { + root, + build: { + outDir: options.build?.outDir ?? path.join(root, "_fresh"), + }, + basePath: options.basePath ?? "", + staticDir: options.staticDir ?? path.join(root, "static"), + mode: MODE, + }; +} + +export function getSnapshotPath(config: ResolvedFreshConfig): string { + return path.join(config.build.outDir, "snapshot.json"); +} diff --git a/src/config_test.ts b/src/config_test.ts new file mode 100644 index 00000000000..1233c3d7897 --- /dev/null +++ b/src/config_test.ts @@ -0,0 +1,14 @@ +import { expect } from "@std/expect"; +import { parseRootPath } from "./config.ts"; + +// FIXME: Windows +Deno.test.ignore("parseRootPath", () => { + expect(parseRootPath("file:///foo/bar")).toEqual("/foo/bar"); + expect(parseRootPath("file:///foo/bar.ts")).toEqual("/foo"); + expect(parseRootPath("/foo/bar")).toEqual("/foo/bar"); + expect(parseRootPath("/foo/bar.ts")).toEqual("/foo"); + expect(parseRootPath("/foo/bar.tsx")).toEqual("/foo"); + expect(parseRootPath("/foo/bar.js")).toEqual("/foo"); + expect(parseRootPath("/foo/bar.jsx")).toEqual("/foo"); + expect(parseRootPath("/foo/bar.mjs")).toEqual("/foo"); +}); diff --git a/src/constants.ts b/src/constants.ts index ae57771bd58..e075f235df0 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,13 +1,5 @@ -export const PARTIAL_SEARCH_PARAM = "fresh-partial"; -export const PARTIAL_ATTR = "f-partial"; -export const LOADING_ATTR = "f-loading"; -export const CLIENT_NAV_ATTR = "f-client-nav"; -export const DATA_KEY_ATTR = "data-fresh-key"; -export const DATA_CURRENT = "data-current"; -export const DATA_ANCESTOR = "data-ancestor"; - -export const enum PartialMode { - REPLACE, - APPEND, - PREPEND, -} +export const INTERNAL_PREFIX = "/_frsh"; +export const DEV_CLIENT_URL = `${INTERNAL_PREFIX}/fresh_dev_client.js`; +export const DEV_ERROR_OVERLAY_URL = `${INTERNAL_PREFIX}/error_overlay`; +export const ALIVE_URL = `${INTERNAL_PREFIX}/alive`; +export const JS_PREFIX = `/js`; diff --git a/src/context.ts b/src/context.ts new file mode 100644 index 00000000000..5c4fb5bebf5 --- /dev/null +++ b/src/context.ts @@ -0,0 +1,203 @@ +import { type ComponentType, h, isValidElement, type VNode } from "preact"; +import type { ResolvedFreshConfig } from "./config.ts"; +import { renderToString } from "preact-render-to-string"; +import type { BuildCache } from "./build_cache.ts"; +import { + FreshScripts, + RenderState, + setRenderState, +} from "./runtime/server/preact_hooks.tsx"; +import { DEV_ERROR_OVERLAY_URL } from "./constants.ts"; + +export interface Island { + file: string | URL; + name: string; + exportName: string; + fn: ComponentType; +} + +export type ServerIslandRegistry = Map; + +/** + * The context passed to every middleware. It is unique for every request. + */ +export interface FreshContext { + /** Reference to the resolved Fresh configuration */ + readonly config: ResolvedFreshConfig; + state: State; + data: Data; + /** The original incoming `Request` object` */ + req: Request; + /** + * The request url parsed into an `URL` instance. This is typically used + * to apply logic based on the pathname of the incoming url or when + * certain search parameters are set. + */ + url: URL; + params: Record; + error: unknown; + info?: Deno.ServeHandlerInfo | Deno.ServeUnixHandlerInfo; + /** + * Return a redirect response to the specified path. This is the + * preferred way to do redirects in Fresh. + * + * ```ts + * ctx.redirect("/foo/bar") // redirect user to "/foo/bar" + * + * // Disallows protocol relative URLs for improved security. This + * // redirects the user to `/evil.com` which is safe, + * // instead of redirecting to `http://evil.com`. + * ctx.redirect("//evil.com/"); + * ``` + */ + redirect(path: string, status?: number): Response; + /** + * Call the next middleware. + * ```ts + * const myMiddleware: Middleware = (ctx) => { + * // do something + * + * // Call the next middleware + * return ctx.next(); + * } + * + * const myMiddleware2: Middleware = async (ctx) => { + * // do something before the next middleware + * doSomething() + * + * const res = await ctx.next(); + * + * // do something after the middleware + * doSomethingAfter() + * + * // Return the `Response` + * return res + * } + */ + next(): Promise; + render(vnode: VNode, init?: ResponseInit): Response | Promise; +} + +export let getBuildCache: (ctx: FreshContext) => BuildCache; + +export class FreshReqContext implements FreshContext { + url: URL; + params = {} as Record; + state = {} as State; + data = {} as never; + error: Error | null = null; + #islandRegistry: ServerIslandRegistry; + #buildCache: BuildCache; + + static { + getBuildCache = (ctx) => (ctx as FreshReqContext).#buildCache; + } + + constructor( + public req: Request, + public config: ResolvedFreshConfig, + public next: FreshContext["next"], + islandRegistry: ServerIslandRegistry, + buildCache: BuildCache, + public info: Deno.ServeHandlerInfo | Deno.ServeUnixHandlerInfo, + ) { + this.#islandRegistry = islandRegistry; + this.#buildCache = buildCache; + this.url = new URL(req.url); + } + + redirect(pathOrUrl: string, status = 302): Response { + let location = pathOrUrl; + + // Disallow protocol relative URLs + if (pathOrUrl !== "/" && pathOrUrl.startsWith("/")) { + let idx = pathOrUrl.indexOf("?"); + if (idx === -1) { + idx = pathOrUrl.indexOf("#"); + } + + const pathname = idx > -1 ? pathOrUrl.slice(0, idx) : pathOrUrl; + const search = idx > -1 ? pathOrUrl.slice(idx) : ""; + + // Remove double slashes to prevent open redirect vulnerability. + location = `${pathname.replaceAll(/\/+/g, "/")}${search}`; + } + + return new Response(null, { + status, + headers: { + location, + }, + }); + } + + render( + // deno-lint-ignore no-explicit-any + vnode: VNode, + init: ResponseInit | undefined = {}, + ): Response | Promise { + if (arguments.length === 0) { + throw new Error(`No arguments passed to: ctx.render()`); + } else if (vnode !== null && !isValidElement(vnode)) { + throw new Error(`Non-JSX element passed to: ctx.render()`); + } + + const headers = init.headers !== undefined + ? init.headers instanceof Headers + ? init.headers + : new Headers(init.headers) + : new Headers(); + + headers.set("Content-Type", "text/html; charset=utf-8"); + const responseInit: ResponseInit = { status: init.status ?? 200, headers }; + + let partialId = ""; + if (this.url.searchParams.has("fresh-partial")) { + partialId = crypto.randomUUID(); + headers.set("X-Fresh-Id", partialId); + } + + const html = preactRender( + vnode, + this, + this.#islandRegistry, + this.#buildCache, + partialId, + ); + return new Response(html, responseInit); + } +} + +function preactRender( + vnode: VNode, + ctx: FreshContext, + islandRegistry: ServerIslandRegistry, + buildCache: BuildCache, + partialId: string, +) { + const state = new RenderState(ctx, islandRegistry, buildCache, partialId); + setRenderState(state); + try { + let res = renderToString(vnode); + // We require a the full outer DOM structure so that browser put + // comment markers in the right place in the DOM. + if (!state.renderedHtmlBody) { + let scripts = ""; + if (ctx.url.pathname !== ctx.config.basePath + DEV_ERROR_OVERLAY_URL) { + scripts = renderToString(h(FreshScripts, null)); + } + res = `${res}${scripts}`; + } + if (!state.renderedHtmlHead) { + res = `${res}`; + } + if (!state.renderedHtmlTag) { + res = `${res}`; + } + + return `${res}`; + } finally { + state.clear(); + setRenderState(null); + } +} diff --git a/src/context_test.tsx b/src/context_test.tsx new file mode 100644 index 00000000000..773f7ddf4de --- /dev/null +++ b/src/context_test.tsx @@ -0,0 +1,71 @@ +import { expect } from "@std/expect"; +import { FreshReqContext } from "./context.ts"; +import { App } from "@fresh/core"; +import { asset } from "@fresh/core/runtime"; +import { FakeServer } from "./test_utils.ts"; +import { BUILD_ID } from "./runtime/build_id.ts"; +import { parseHtml } from "../tests/test_utils.tsx"; + +Deno.test("FreshReqContext.prototype.redirect", () => { + let res = FreshReqContext.prototype.redirect("/"); + expect(res.status).toEqual(302); + expect(res.headers.get("Location")).toEqual("/"); + + res = FreshReqContext.prototype.redirect("//evil.com"); + expect(res.status).toEqual(302); + expect(res.headers.get("Location")).toEqual("/evil.com"); + + res = FreshReqContext.prototype.redirect("//evil.com/foo//bar"); + expect(res.status).toEqual(302); + expect(res.headers.get("Location")).toEqual("/evil.com/foo/bar"); + + res = FreshReqContext.prototype.redirect("https://deno.com"); + expect(res.status).toEqual(302); + expect(res.headers.get("Location")).toEqual("https://deno.com"); + + res = FreshReqContext.prototype.redirect("/", 307); + expect(res.status).toEqual(307); +}); + +Deno.test("render asset()", async () => { + const app = new App() + .get("/", (ctx) => + ctx.render( + <> +

{asset("/foo")}

+ + + , + )); + + const server = new FakeServer(await app.handler()); + const res = await server.get("/"); + const doc = parseHtml(await res.text()); + + expect(doc.querySelector(".raw")!.textContent).toContain(BUILD_ID); + expect(doc.querySelector("img")!.src).toContain(BUILD_ID); + expect(doc.querySelector("img")!.srcset).toContain(BUILD_ID); + expect(doc.querySelector("source")!.src).toContain(BUILD_ID); +}); + +Deno.test("ctx.render - throw with no arguments", async () => { + const app = new App() + // deno-lint-ignore no-explicit-any + .get("/", (ctx) => (ctx as any).render()); + const server = new FakeServer(await app.handler()); + const res = await server.get("/"); + + await res.body?.cancel(); + expect(res.status).toEqual(500); +}); + +Deno.test("ctx.render - throw with invalid first arg", async () => { + const app = new App() + // deno-lint-ignore no-explicit-any + .get("/", (ctx) => (ctx as any).render({})); + const server = new FakeServer(await app.handler()); + const res = await server.get("/"); + + await res.body?.cancel(); + expect(res.status).toEqual(500); +}); diff --git a/src/dev/build.ts b/src/dev/build.ts deleted file mode 100644 index 32d4a56ef7d..00000000000 --- a/src/dev/build.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { getServerContext } from "../server/context.ts"; -import { join } from "../server/deps.ts"; -import { colors, emptyDir } from "./deps.ts"; -import { BuildSnapshotJson } from "../build/mod.ts"; -import { BUILD_ID } from "../server/build_id.ts"; -import { InternalFreshState } from "../server/types.ts"; - -export async function build( - state: InternalFreshState, -) { - const outDir = state.config.build.outDir; - const plugins = state.config.plugins; - - // Ensure that build dir is empty - await emptyDir(outDir); - - // Create a directory for static assets produced during the build - await Deno.mkdir(join(outDir, "static")); - - await Promise.all( - plugins.map((plugin) => plugin.configResolved?.(state.config)), - ); - await Promise.all(plugins.map((plugin) => plugin.buildStart?.(state.config))); - - // Bundle assets - const ctx = await getServerContext(state); - const snapshot = await ctx.buildSnapshot(); - - // Write output files to disk - await Promise.all(snapshot.paths.map(async (fileName) => { - const data = await snapshot.read(fileName); - if (data === null) return; - - return Deno.writeFile(join(outDir, fileName), data); - })); - - // Write dependency snapshot file to disk - const jsonSnapshot: BuildSnapshotJson = { - build_id: BUILD_ID, - files: {}, - }; - for (const filePath of snapshot.paths) { - const dependencies = snapshot.dependencies(filePath); - jsonSnapshot.files[filePath] = dependencies; - } - - const snapshotPath = join(outDir, "snapshot.json"); - await Deno.writeTextFile(snapshotPath, JSON.stringify(jsonSnapshot, null, 2)); - - console.log( - `Assets written to: ${colors.green(outDir)}`, - ); - - await Promise.all(plugins.map((plugin) => plugin.buildEnd?.())); -} diff --git a/src/dev/builder.ts b/src/dev/builder.ts new file mode 100644 index 00000000000..9a59a670472 --- /dev/null +++ b/src/dev/builder.ts @@ -0,0 +1,250 @@ +import { + App, + getBuildCache, + getIslandRegistry, + type ListenOptions, + setBuildCache, +} from "../app.ts"; +import { fsAdapter } from "../fs.ts"; +import * as path from "@std/path"; +import * as colors from "@std/fmt/colors"; +import { bundleJs } from "./esbuild.ts"; +import * as JSONC from "@std/jsonc"; +import { liveReload } from "./middlewares/live_reload.ts"; +import { + cssAssetHash, + FreshFileTransformer, + type OnTransformOptions, +} from "./file_transformer.ts"; +import type { TransformFn } from "./file_transformer.ts"; +import { DiskBuildCache, MemoryBuildCache } from "./dev_build_cache.ts"; +import type { Island } from "../context.ts"; +import { BUILD_ID } from "../runtime/build_id.ts"; +import { updateCheck } from "./update_check.ts"; +import { DAY } from "@std/datetime"; +import { devErrorOverlay } from "./middlewares/error_overlay/middleware.tsx"; + +export interface BuildOptions { + /** + * This sets the target environment for the generated code. Newer + * language constructs will be transformed to match the specified + * support range. See https://esbuild.github.io/api/#target + * @default {"es2022"} + */ + target?: string | string[]; +} + +export interface FreshBuilder { + onTransformStaticFile( + options: OnTransformOptions, + callback: TransformFn, + ): void; + build(app: App, options?: BuildOptions): Promise; + listen(app: App, options?: ListenOptions & BuildOptions): Promise; +} + +export class Builder implements FreshBuilder { + #transformer = new FreshFileTransformer(fsAdapter); + #addedInternalTransforms = false; + #options: { target: string | string[] }; + + constructor(options: BuildOptions = {}) { + this.#options = { + target: options.target ?? ["chrome99", "firefox99", "safari15"], + }; + } + + onTransformStaticFile( + options: OnTransformOptions, + callback: TransformFn, + ): void { + this.#transformer.onTransform(options, callback); + } + + async listen(app: App, options: ListenOptions = {}): Promise { + // Run update check in background + updateCheck(DAY).catch(() => {}); + + const devApp = new App(app.config) + .use(liveReload()) + .use(devErrorOverlay()) + .mountApp("*", app); + + setBuildCache( + devApp, + new MemoryBuildCache( + devApp.config, + BUILD_ID, + this.#transformer, + this.#options.target, + ), + ); + + await Promise.all([ + devApp.listen(options), + this.#build(devApp, true), + ]); + return; + } + + async build(app: App): Promise { + setBuildCache( + app, + new DiskBuildCache( + app.config, + BUILD_ID, + this.#transformer, + this.#options.target, + ), + ); + return await this.#build(app, false); + } + + async #build(app: App, dev: boolean): Promise { + const { build } = app.config; + const staticOutDir = path.join(build.outDir, "static"); + + if (!this.#addedInternalTransforms) { + this.#addedInternalTransforms = true; + cssAssetHash(this.#transformer); + } + + const target = this.#options.target; + + try { + await Deno.remove(staticOutDir); + } catch { + // Ignore + } + + const buildCache = getBuildCache(app)! as MemoryBuildCache | DiskBuildCache; + + const entryPoints: Record = { + "fresh-runtime": dev ? "@fresh/core/client-dev" : "@fresh/core/client", + }; + const seenEntries = new Map(); + const mapIslandToEntry = new Map(); + const islandRegistry = getIslandRegistry(app); + for (const island of islandRegistry.values()) { + const filePath = island.file instanceof URL + ? island.file.href + : island.file; + + const seen = seenEntries.get(filePath); + if (seen !== undefined) { + mapIslandToEntry.set(island, seen.name); + } else { + entryPoints[island.name] = filePath; + seenEntries.set(filePath, island); + mapIslandToEntry.set(island, island.name); + } + } + + const denoJson = await readDenoConfig(app.config.root); + + const jsxImportSource = denoJson.config.compilerOptions?.jsxImportSource; + if (jsxImportSource === undefined) { + throw new Error( + `Option compilerOptions > jsxImportSource not set in: ${denoJson.filePath}`, + ); + } + + // Check precompile option + if (denoJson.config.compilerOptions?.jsx === "precompile") { + const expected = ["a", "img", "source", "body", "html", "head"]; + const skipped = denoJson.config.compilerOptions.jsxPrecompileSkipElements; + if (!skipped || expected.some((name) => !skipped.includes(name))) { + throw new Error( + `Expected option compilerOptions > jsxPrecompileSkipElements to contain ${ + expected.map((name) => `"${name}"`).join(", ") + }`, + ); + } + } + + const output = await bundleJs({ + cwd: app.config.root, + outDir: staticOutDir, + dev: dev ?? false, + target, + buildId: BUILD_ID, + entryPoints, + jsxImportSource, + denoJsonPath: denoJson.filePath, + }); + + for (let i = 0; i < output.files.length; i++) { + const file = output.files[i]; + const pathname = `/${file.path}`; + await buildCache.addProcessedFile(pathname, file.contents, file.hash); + } + + // Go through same entry islands + for (const [island, entry] of mapIslandToEntry.entries()) { + const chunk = output.entryToChunk.get(entry); + if (chunk === undefined) { + throw new Error( + `Missing chunk for ${island.file}#${island.exportName}`, + ); + } + buildCache.islands.set(island.name, `/${chunk}`); + } + + await buildCache.flush(); + + if (!dev) { + console.log( + `Assets written to: ${colors.cyan(build.outDir)}`, + ); + } + } +} + +export interface DenoConfig { + imports?: Record; + importMap?: string; + tasks?: Record; + lint?: { + rules: { tags?: string[] }; + exclude?: string[]; + }; + fmt?: { + exclude?: string[]; + }; + exclude?: string[]; + compilerOptions?: { + jsx?: string; + jsxImportSource?: string; + jsxPrecompileSkipElements?: string[]; + }; +} + +export async function readDenoConfig( + directory: string, +): Promise<{ config: DenoConfig; filePath: string }> { + let dir = directory; + while (true) { + for (const name of ["deno.json", "deno.jsonc"]) { + const filePath = path.join(dir, name); + try { + const file = await Deno.readTextFile(filePath); + if (name.endsWith(".jsonc")) { + return { config: JSONC.parse(file) as DenoConfig, filePath }; + } else { + return { config: JSON.parse(file), 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 deno.json file in the current directory or any parent directory.`, + ); + } + dir = parent; + } +} diff --git a/src/dev/builder_test.ts b/src/dev/builder_test.ts new file mode 100644 index 00000000000..249be227e86 --- /dev/null +++ b/src/dev/builder_test.ts @@ -0,0 +1,65 @@ +import { expect } from "@std/expect"; +import * as path from "@std/path"; +import { Builder } from "./builder.ts"; +import { App } from "../app.ts"; + +Deno.test("Builder - chain onTransformStaticFile", async () => { + const logs: string[] = []; + const builder = new Builder(); + builder.onTransformStaticFile( + { pluginName: "A", filter: /\.css$/ }, + () => { + logs.push("A"); + }, + ); + builder.onTransformStaticFile( + { pluginName: "B", filter: /\.css$/ }, + () => { + logs.push("B"); + }, + ); + builder.onTransformStaticFile( + { pluginName: "C", filter: /\.css$/ }, + () => { + logs.push("C"); + }, + ); + + const tmp = await Deno.makeTempDir(); + await Deno.writeTextFile(path.join(tmp, "foo.css"), "body { color: red; }"); + const app = new App({ + staticDir: tmp, + build: { + outDir: path.join(tmp, "dist"), + }, + }); + await builder.build(app); + + expect(logs).toEqual(["A", "B", "C"]); +}); + +Deno.test({ + name: "Builder - hashes CSS urls by default", + fn: async () => { + const builder = new Builder(); + const tmp = await Deno.makeTempDir(); + await Deno.writeTextFile( + path.join(tmp, "foo.css"), + "body { background: url('/foo.jpg'); }", + ); + const app = new App({ + staticDir: tmp, + build: { + outDir: path.join(tmp, "dist"), + }, + }); + await builder.build(app); + + const css = await Deno.readTextFile( + path.join(tmp, "dist", "static", "foo.css"), + ); + expect(css).toContain('body { background: url("/foo.jpg?__frsh_c='); + }, + sanitizeOps: false, + sanitizeResources: false, +}); diff --git a/src/dev/cli.ts b/src/dev/cli.ts deleted file mode 100644 index e00352deca0..00000000000 --- a/src/dev/cli.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { join, toFileUrl } from "./deps.ts"; -import { manifest } from "./mod.ts"; -import { type FreshConfig } from "../server/mod.ts"; - -const args = Deno.args; - -switch (args[0]) { - case "manifest": { - if (args[1]) { - const CONFIG_TS_PATH = join(args[1], "fresh.config.ts"); - const url = toFileUrl(CONFIG_TS_PATH).toString(); - const config: FreshConfig = (await import(url)).default; - await manifest(args[1], config?.router?.ignoreFilePattern); - } else { - console.error("Missing input for manifest command"); - Deno.exit(1); - } - break; - } - default: { - console.error("Invalid command"); - Deno.exit(1); - } -} diff --git a/src/dev/deps.ts b/src/dev/deps.ts deleted file mode 100644 index 707aad328e8..00000000000 --- a/src/dev/deps.ts +++ /dev/null @@ -1,32 +0,0 @@ -// std -export { - basename, - dirname, - extname, - fromFileUrl, - join, - relative, - resolve, - SEPARATOR, - toFileUrl, -} from "https://deno.land/std@0.216.0/path/mod.ts"; -export { normalize } from "https://deno.land/std@0.216.0/path/posix/mod.ts"; -export { DAY, WEEK } from "https://deno.land/std@0.216.0/datetime/constants.ts"; -export * as colors from "https://deno.land/std@0.216.0/fmt/colors.ts"; -export { - walk, - type WalkEntry, - WalkError, -} from "https://deno.land/std@0.216.0/fs/walk.ts"; -export { parse } from "https://deno.land/std@0.216.0/flags/mod.ts"; -export { - greaterOrEqual, - lessThan, - parse as semverParse, -} from "https://deno.land/std@0.216.0/semver/mod.ts"; -export { emptyDir, existsSync } from "https://deno.land/std@0.216.0/fs/mod.ts"; -export * as JSONC from "https://deno.land/std@0.216.0/jsonc/mod.ts"; -export { assertEquals } from "https://deno.land/std@0.216.0/assert/mod.ts"; - -// ts-morph -export { Node, Project } from "https://deno.land/x/ts_morph@21.0.1/mod.ts"; diff --git a/src/dev/dev_build_cache.ts b/src/dev/dev_build_cache.ts new file mode 100644 index 00000000000..5f200683848 --- /dev/null +++ b/src/dev/dev_build_cache.ts @@ -0,0 +1,292 @@ +import type { BuildCache, StaticFile } from "../build_cache.ts"; +import * as path from "@std/path"; +import type { ResolvedFreshConfig } from "../config.ts"; +import type { BuildSnapshot } from "../build_cache.ts"; +import { encodeHex } from "@std/encoding/hex"; +import { crypto } from "@std/crypto"; +import { fsAdapter } from "../fs.ts"; +import type { FreshFileTransformer } from "./file_transformer.ts"; +import { assertInDir } from "../utils.ts"; + +export interface MemoryFile { + hash: string | null; + content: Uint8Array; +} + +export interface DevBuildCache extends BuildCache { + islands: Map; + + addUnprocessedFile(pathname: string): void; + + addProcessedFile( + pathname: string, + content: Uint8Array, + hash: string | null, + ): Promise; + + flush(): Promise; +} + +export class MemoryBuildCache implements DevBuildCache { + hasSnapshot = true; + islands = new Map(); + #processedFiles = new Map(); + #unprocessedFiles = new Map(); + #ready = Promise.withResolvers(); + + constructor( + public config: ResolvedFreshConfig, + public buildId: string, + public transformer: FreshFileTransformer, + public target: string | string[], + ) { + } + + async readFile(pathname: string): Promise { + await this.#ready.promise; + const processed = this.#processedFiles.get(pathname); + if (processed !== undefined) { + return { + hash: processed.hash, + readable: processed.content, + size: processed.content.byteLength, + }; + } + + const unprocessed = this.#unprocessedFiles.get(pathname); + if (unprocessed !== undefined) { + try { + const [stat, file] = await Promise.all([ + Deno.stat(unprocessed), + Deno.open(unprocessed, { read: true }), + ]); + + return { + hash: null, + size: stat.size, + readable: file.readable, + }; + } catch (_err) { + return null; + } + } + + let entry = pathname.startsWith("/") ? pathname.slice(1) : pathname; + entry = path.join(this.config.staticDir, entry); + const relative = path.relative(this.config.staticDir, entry); + if (relative.startsWith(".")) { + throw new Error( + `Processed file resolved outside of static dir ${entry}`, + ); + } + + // Might be a file that we still need to process + const transformed = await this.transformer.process( + entry, + "development", + this.target, + ); + + if (transformed !== null) { + for (let i = 0; i < transformed.length; i++) { + const file = transformed[i]; + const relative = path.relative(this.config.staticDir, file.path); + if (relative.startsWith(".")) { + throw new Error( + `Processed file resolved outside of static dir ${file.path}`, + ); + } + const pathname = `/${relative}`; + + this.addProcessedFile(pathname, file.content, null); + } + if (this.#processedFiles.has(pathname)) { + return this.readFile(pathname); + } + } else { + try { + const filePath = path.join(this.config.staticDir, pathname); + const relative = path.relative(this.config.staticDir, filePath); + if (!relative.startsWith(".") && (await Deno.stat(filePath)).isFile) { + this.addUnprocessedFile(pathname); + return this.readFile(pathname); + } + } catch (err) { + if (!(err instanceof Deno.errors.NotFound)) { + throw err; + } + } + } + + return null; + } + + getIslandChunkName(islandName: string): string | null { + return this.islands.get(islandName) ?? null; + } + + addUnprocessedFile(pathname: string): void { + this.#unprocessedFiles.set( + pathname, + path.join(this.config.staticDir, pathname), + ); + } + + // deno-lint-ignore require-await + async addProcessedFile( + pathname: string, + content: Uint8Array, + hash: string | null, + ): Promise { + this.#processedFiles.set(pathname, { content, hash }); + } + + // deno-lint-ignore require-await + async flush(): Promise { + this.#ready.resolve(); + } +} + +// await fsAdapter.mkdirp(staticOutDir); +export class DiskBuildCache implements DevBuildCache { + hasSnapshot = true; + islands = new Map(); + #processedFiles = new Map(); + #unprocessedFiles = new Map(); + #transformer: FreshFileTransformer; + #target: string | string[]; + + constructor( + public config: ResolvedFreshConfig, + public buildId: string, + transformer: FreshFileTransformer, + target: string | string[], + ) { + this.#transformer = transformer; + this.#target = target; + } + + getIslandChunkName(islandName: string): string | null { + return this.islands.get(islandName) ?? null; + } + + addUnprocessedFile(pathname: string): void { + this.#unprocessedFiles.set( + pathname, + path.join(this.config.staticDir, pathname), + ); + } + + async addProcessedFile( + pathname: string, + content: Uint8Array, + hash: string | null, + ) { + this.#processedFiles.set(pathname, hash); + + const outDir = pathname === "/metafile.json" + ? this.config.build.outDir + : path.join(this.config.build.outDir, "static"); + const filePath = path.join(outDir, pathname); + assertInDir(filePath, outDir); + + await fsAdapter.mkdirp(path.dirname(filePath)); + await Deno.writeFile(filePath, content); + } + + // deno-lint-ignore require-await + async readFile(_pathname: string): Promise { + throw new Error("Not implemented in build mode"); + } + + async flush(): Promise { + const staticDir = this.config.staticDir; + + if (await fsAdapter.isDirectory(staticDir)) { + const entries = fsAdapter.walk(staticDir, { + includeDirs: false, + includeFiles: true, + followSymlinks: false, + // Skip any folder or file starting with a "." + skip: [/\/\.[^/]+(\/|$)/], + }); + + for await (const entry of entries) { + const result = await this.#transformer.process( + entry.path, + "production", + this.#target, + ); + + if (result !== null) { + for (let i = 0; i < result.length; i++) { + const file = result[i]; + assertInDir(file.path, staticDir); + const pathname = `/${path.relative(staticDir, file.path)}`; + await this.addProcessedFile(pathname, file.content, null); + } + } else { + const relative = path.relative(staticDir, entry.path); + const pathname = `/${relative}`; + this.addUnprocessedFile(pathname); + } + } + } + + const snapshot: BuildSnapshot = { + version: 1, + buildId: this.buildId, + islands: {}, + staticFiles: {}, + }; + + for (const [name, chunk] of this.islands.entries()) { + snapshot.islands[name] = chunk; + } + + for (const [name, filePath] of this.#unprocessedFiles.entries()) { + const file = await Deno.open(filePath); + const hash = await hashContent(file.readable); + + snapshot.staticFiles[name] = { + hash, + generated: false, + }; + } + + for (const [name, maybeHash] of this.#processedFiles.entries()) { + let hash = maybeHash; + + // Ignore esbuild meta file. It's not intended for serving + if (name === "/metafile.json") { + continue; + } + + if (maybeHash === null) { + const filePath = path.join(this.config.build.outDir, "static", name); + const file = await Deno.open(filePath); + hash = await hashContent(file.readable); + } + + snapshot.staticFiles[name] = { + hash, + generated: true, + }; + } + + await Deno.writeTextFile( + path.join(this.config.build.outDir, "snapshot.json"), + JSON.stringify(snapshot, null, 2), + ); + } +} + +async function hashContent( + content: Uint8Array | ReadableStream, +): Promise { + const hashBuf = await crypto.subtle.digest( + "SHA-256", + content, + ); + return encodeHex(hashBuf); +} diff --git a/src/dev/dev_command.ts b/src/dev/dev_command.ts deleted file mode 100644 index 591dd2b3df1..00000000000 --- a/src/dev/dev_command.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { updateCheck } from "./update_check.ts"; -import { DAY, dirname, fromFileUrl, join, toFileUrl } from "./deps.ts"; -import { FreshConfig, Manifest as ServerManifest } from "../server/mod.ts"; -import { build } from "./build.ts"; -import { collect, ensureMinDenoVersion, generate, Manifest } from "./mod.ts"; -import { startServer } from "../server/boot.ts"; -import { getInternalFreshState } from "../server/config.ts"; -import { getServerContext } from "../server/context.ts"; - -export async function dev( - base: string, - entrypoint: string, - config?: FreshConfig, -) { - ensureMinDenoVersion(); - - // Run update check in background - updateCheck(DAY).catch(() => {}); - - const dir = dirname(fromFileUrl(base)); - - let currentManifest: Manifest; - const prevManifest = Deno.env.get("FRSH_DEV_PREVIOUS_MANIFEST"); - if (prevManifest) { - currentManifest = JSON.parse(prevManifest); - } else { - currentManifest = { islands: [], routes: [] }; - } - const newManifest = await collect(dir, config?.router?.ignoreFilePattern); - Deno.env.set("FRSH_DEV_PREVIOUS_MANIFEST", JSON.stringify(newManifest)); - - const manifestChanged = - !arraysEqual(newManifest.routes, currentManifest.routes) || - !arraysEqual(newManifest.islands, currentManifest.islands); - - if (manifestChanged) await generate(dir, newManifest); - - const manifest = (await import(toFileUrl(join(dir, "fresh.gen.ts")).href)) - .default as ServerManifest; - - if (Deno.args.includes("build")) { - const state = await getInternalFreshState( - manifest, - config ?? {}, - ); - state.config.dev = false; - state.loadSnapshot = false; - state.build = true; - await build(state); - } else if (config) { - const state = await getInternalFreshState( - manifest, - config, - ); - state.config.dev = true; - state.loadSnapshot = false; - const ctx = await getServerContext(state); - await startServer(ctx.handler(), { - ...state.config.server, - basePath: state.config.basePath, - }); - } else { - // Legacy entry point: Back then `dev.ts` would call `main.ts` but - // this causes duplicate plugin instantiation if both `dev.ts` and - // `main.ts` instantiate plugins. - Deno.env.set("__FRSH_LEGACY_DEV", "true"); - entrypoint = new URL(entrypoint, base).href; - await import(entrypoint); - } -} - -function arraysEqual(a: T[], b: T[]): boolean { - if (a.length !== b.length) return false; - for (let i = 0; i < a.length; ++i) { - if (a[i] !== b[i]) return false; - } - return true; -} diff --git a/src/dev/error.ts b/src/dev/error.ts deleted file mode 100644 index 7f7d72243ee..00000000000 --- a/src/dev/error.ts +++ /dev/null @@ -1,8 +0,0 @@ -export function printError(message: string) { - console.error(`%cerror%c: ${message}`, "color: red; font-weight: bold", ""); -} - -export function error(message: string): never { - printError(message); - Deno.exit(1); -} diff --git a/src/dev/esbuild.ts b/src/dev/esbuild.ts new file mode 100644 index 00000000000..b587b610d5f --- /dev/null +++ b/src/dev/esbuild.ts @@ -0,0 +1,201 @@ +import { denoPlugins } from "@luca/esbuild-deno-loader"; +import type { Plugin as EsbuildPlugin } from "esbuild"; +import * as path from "@std/path"; + +export interface FreshBundleOptions { + dev: boolean; + cwd: string; + buildId: string; + outDir: string; + denoJsonPath: string; + entryPoints: Record; + target: string | string[]; + jsxImportSource?: string; +} + +export interface BuildOutput { + entryToChunk: Map; + dependencies: Map; + files: Array<{ hash: string | null; contents: Uint8Array; path: string }>; +} + +let esbuild: null | typeof import("esbuild-wasm") = null; + +const PREACT_ENV = Deno.env.get("PREACT_PATH"); + +export async function bundleJs( + options: FreshBundleOptions, +): Promise { + if (esbuild === null) { + esbuild = Deno.env.get("FRESH_ESBUILD_LOADER") === "portable" + ? await import("esbuild-wasm") + : await import("esbuild"); + + await esbuild.initialize({}); + } + + try { + await Deno.mkdir(options.cwd, { recursive: true }); + } catch (err) { + if (!(err instanceof Deno.errors.AlreadyExists)) { + throw err; + } + } + + const bundle = await esbuild.build({ + entryPoints: options.entryPoints, + + platform: "browser", + target: options.target, + + format: "esm", + bundle: true, + splitting: true, + treeShaking: true, + sourcemap: options.dev ? "linked" : false, + minify: !options.dev, + + jsxDev: options.dev, + jsx: "automatic", + jsxImportSource: options.jsxImportSource ?? "preact", + + absWorkingDir: options.cwd, + outdir: ".", + write: false, + metafile: true, + + plugins: [ + preactDebugger(PREACT_ENV), + buildIdPlugin(options.buildId), + windowsPathFixer(), + ...denoPlugins({ configPath: options.denoJsonPath }), + ], + }); + + const files: BuildOutput["files"] = []; + for (let i = 0; i < bundle.outputFiles.length; i++) { + const outputFile = bundle.outputFiles[i]; + const relative = path.relative(options.cwd, outputFile.path); + files.push({ + path: relative, + contents: outputFile.contents, + hash: outputFile.hash, + }); + } + + files.push({ + path: "metafile.json", + contents: new TextEncoder().encode(JSON.stringify(bundle.metafile)), + hash: null, + }); + + const entryToChunk = new Map(); + const dependencies = new Map(); + + const entryToName = new Map( + Array.from(Object.entries(options.entryPoints)).map( + (entry) => [entry[1], entry[0]], + ), + ); + + if (bundle.metafile) { + const metaOutputs = new Map(Object.entries(bundle.metafile.outputs)); + + for (const [entryPath, entry] of metaOutputs.entries()) { + const imports = entry.imports + .filter(({ kind }) => kind === "import-statement") + .map(({ path }) => path); + dependencies.set(entryPath, imports); + + if (entryPath !== "fresh-runtime.js" && entry.entryPoint !== undefined) { + const filePath = path.join(options.cwd, entry.entryPoint); + + const name = entryToName.get(filePath)!; + entryToChunk.set(name, entryPath); + } + } + } + + if (!options.dev) { + await esbuild.stop(); + } + + return { + files, + entryToChunk, + dependencies, + }; +} + +function buildIdPlugin(buildId: string): EsbuildPlugin { + return { + name: "fresh-build-id", + setup(build) { + build.onResolve({ filter: /runtime[/\\]+build_id\.ts$/ }, (args) => { + return { + path: args.path, + namespace: "fresh-internal", + }; + }); + build.onLoad({ + filter: /runtime[/\\]build_id\.ts$/, + namespace: "fresh-internal", + }, () => { + return { + contents: `export const BUILD_ID = "${buildId}";`, + }; + }); + }, + }; +} + +function toPreactModPath(mod: string): string { + if (mod === "preact/debug") { + return path.join("debug", "dist", "debug.module.js"); + } else if (mod === "preact/hooks") { + return path.join("hooks", "dist", "hooks.module.js"); + } else if (mod === "preact/devtools") { + return path.join("devtools", "dist", "devtools.module.js"); + } else if (mod === "preact/compat") { + return path.join("compat", "dist", "compat.module.js"); + } else if (mod === "preact/jsx-runtime" || mod === "preact/jsx-dev-runtime") { + return path.join("jsx-runtime", "dist", "jsxRuntime.module.js"); + } else { + return path.join("dist", "preact.module.js"); + } +} + +function preactDebugger(preactPath: string | undefined): EsbuildPlugin { + return { + name: "fresh-preact-debugger", + setup(build) { + if (preactPath === undefined) return; + + build.onResolve({ filter: /^preact/ }, (args) => { + const resolved = path.resolve(preactPath, toPreactModPath(args.path)); + + return { + path: resolved, + }; + }); + }, + }; +} + +function windowsPathFixer(): EsbuildPlugin { + return { + name: "fresh-fix-windows", + setup(build) { + if (Deno.build.os === "windows") { + build.onResolve({ filter: /\.*/ }, (args) => { + if (args.path.startsWith("\\")) { + const normalized = path.resolve(args.path); + return { + path: normalized, + }; + } + }); + } + }, + }; +} diff --git a/src/dev/file_transformer.ts b/src/dev/file_transformer.ts new file mode 100644 index 00000000000..2335f71768d --- /dev/null +++ b/src/dev/file_transformer.ts @@ -0,0 +1,247 @@ +import type { FsAdapter } from "../fs.ts"; +import { BUILD_ID } from "../runtime/build_id.ts"; +import { assetInternal } from "../runtime/shared_internal.tsx"; + +export type TransformMode = "development" | "production"; + +export interface OnTransformOptions { + pluginName: string; + filter: RegExp; +} + +export interface OnTransformResult { + content: string | Uint8Array; + path?: string; + map?: string | Uint8Array; +} + +export interface OnTransformArgs { + path: string; + target: string | string[]; + text: string; + content: Uint8Array; + mode: TransformMode; +} +export type TransformFn = ( + args: OnTransformArgs, +) => + | void + | OnTransformResult + | Array<{ path: string } & Omit> + | Promise< + | void + | OnTransformResult + | Array<{ path: string } & Omit> + >; + +export interface Transformer { + options: OnTransformOptions; + fn: TransformFn; +} + +export interface ProcessedFile { + path: string; + content: Uint8Array; + map: Uint8Array | null; + inputFiles: string[]; +} + +interface TransformReq { + newFile: boolean; + filePath: string; + content: Uint8Array; + map: null | Uint8Array; + inputFiles: string[]; +} + +export class FreshFileTransformer { + #transformers: Transformer[] = []; + #fs: FsAdapter; + + constructor(fs: FsAdapter) { + this.#fs = fs; + } + + onTransform(options: OnTransformOptions, callback: TransformFn): void { + this.#transformers.push({ options, fn: callback }); + } + + async process( + filePath: string, + mode: TransformMode, + target: string | string[], + ): Promise { + // Pre-check if we have any transformer for this file at all + let hasTransformer = false; + for (let i = 0; i < this.#transformers.length; i++) { + if (this.#transformers[i].options.filter.test(filePath)) { + hasTransformer = true; + break; + } + } + + if (!hasTransformer) { + return null; + } + + let content: Uint8Array; + try { + content = await this.#fs.readFile(filePath); + } catch (err) { + if (err instanceof Deno.errors.NotFound) { + return null; + } + + throw err; + } + + const queue: TransformReq[] = [{ + newFile: false, + content, + filePath, + map: null, + inputFiles: [filePath], + }]; + const outFiles: ProcessedFile[] = []; + + const seen = new Set(); + + let req: TransformReq | undefined = undefined; + while ((req = queue.pop()) !== undefined) { + if (seen.has(req.filePath)) continue; + seen.add(req.filePath); + + let transformed = false; + for (let i = 0; i < this.#transformers.length; i++) { + const transformer = this.#transformers[i]; + + const { options, fn } = transformer; + options.filter.lastIndex = 0; + if (!options.filter.test(req.filePath)) { + continue; + } + + const result = await fn({ + path: req.filePath, + mode, + target, + content: req!.content, + get text() { + return new TextDecoder().decode(req!.content); + }, + }); + + if (result !== undefined) { + if (Array.isArray(result)) { + for (let i = 0; i < result.length; i++) { + const item = result[i]; + if (item.path === undefined) { + throw new Error( + `The ".path" property must be set when returning multiple files in a transformer. [${transformer.options.pluginName}]`, + ); + } + + const outContent = typeof item.content === "string" + ? new TextEncoder().encode(item.content) + : item.content; + + const outMap = item.map !== undefined + ? typeof item.map === "string" + ? new TextEncoder().encode(item.map) + : item.map + : null; + + if (req.filePath === item.path) { + if (req.content === outContent && req.map === outMap) { + continue; + } + + transformed = true; + req.content = outContent; + req.map = outMap; + } else { + let found = false; + for (let i = 0; i < queue.length; i++) { + const req = queue[i]; + if (req.filePath === item.path) { + found = true; + transformed = true; + req.content = outContent; + req.map = outMap; + } + } + + if (!found) { + queue.push({ + newFile: true, + filePath: item.path, + content: outContent, + map: outMap, + inputFiles: req.inputFiles.slice(), + }); + } + } + } + } else { + const outContent = typeof result.content === "string" + ? new TextEncoder().encode(result.content) + : result.content; + + const outMap = result.map !== undefined + ? typeof result.map === "string" + ? new TextEncoder().encode(result.map) + : result.map + : null; + + if (req.content === outContent && req.map === outMap) { + continue; + } + + transformed = true; + req.content = outContent; + req.map = outMap; + req.filePath = result.path ?? req.filePath; + } + } + } + + // TODO: Keep transforming until no one processes anymore + if (transformed || req.newFile) { + outFiles.push({ + content: req.content, + map: req.map, + path: req.filePath, + inputFiles: req.inputFiles, + }); + } + } + + return outFiles.length > 0 ? outFiles : null; + } +} + +const CSS_URL_REGEX = /url\((["'][^'"]+["']|[^)]+)\)/g; + +export function cssAssetHash(transformer: FreshFileTransformer) { + transformer.onTransform({ + pluginName: "fresh-css", + filter: /\.css$/, + }, (args) => { + const replaced = args.text.replaceAll(CSS_URL_REGEX, (_, str) => { + let rawUrl = str; + if (str[0] === "'" || str[0] === '"') { + rawUrl = str.slice(1, -1); + } + + if (rawUrl.length === 0) { + return str; + } + + return `url(${JSON.stringify(assetInternal(rawUrl, BUILD_ID))})`; + }); + + return { + content: replaced, + }; + }); +} diff --git a/src/dev/file_transformer_test.ts b/src/dev/file_transformer_test.ts new file mode 100644 index 00000000000..cc25e3deca0 --- /dev/null +++ b/src/dev/file_transformer_test.ts @@ -0,0 +1,229 @@ +import { expect } from "@std/expect"; +import type { FsAdapter } from "../fs.ts"; +import { + FreshFileTransformer, + type ProcessedFile, +} from "./file_transformer.ts"; +import { delay } from "../test_utils.ts"; + +function testTransformer(files: Record) { + const mockFs: FsAdapter = { + isDirectory: () => Promise.resolve(false), + mkdirp: () => Promise.resolve(), + walk: async function* foo() { + }, + readFile: (file) => { + if (file instanceof URL) throw new Error("Not supported"); + // deno-lint-ignore no-explicit-any + const content = (files as any)[file]; + const buf = new TextEncoder().encode(content); + return Promise.resolve(buf); + }, + }; + return new FreshFileTransformer(mockFs); +} + +function consumeResult(result: ProcessedFile[]) { + const out: { + path: string; + content: string; + map: string | null; + inputFiles: string[]; + }[] = []; + for (let i = 0; i < result.length; i++) { + const file = result[i]; + + out.push({ + path: file.path, + content: typeof file.content === "string" + ? file.content + : new TextDecoder().decode(file.content), + map: file.map !== null + ? typeof file.map === "string" + ? file.map + : new TextDecoder().decode(file.map) + : null, + inputFiles: file.inputFiles, + }); + } + + return out.sort((a, b) => a.path.localeCompare(b.path)); +} + +Deno.test("FileTransformer - transform sync", async () => { + const transformer = testTransformer({ + "foo.txt": "foo", + }); + + transformer.onTransform({ pluginName: "foo", filter: /.*/ }, (args) => { + return { + content: args.text + "bar", + }; + }); + + const result = await transformer.process("foo.txt", "development", ""); + const files = await consumeResult(result!); + expect(files).toEqual([ + { content: "foobar", map: null, path: "foo.txt", inputFiles: ["foo.txt"] }, + ]); +}); + +Deno.test("FileTransformer - transform async", async () => { + const transformer = testTransformer({ + "foo.txt": "foo", + }); + + transformer.onTransform({ pluginName: "foo", filter: /.*/ }, async (args) => { + await delay(1); + return { + content: args.text + "bar", + }; + }); + + const result = await transformer.process("foo.txt", "development", ""); + const files = await consumeResult(result!); + expect(files).toEqual([ + { content: "foobar", map: null, path: "foo.txt", inputFiles: ["foo.txt"] }, + ]); +}); + +Deno.test("FileTransformer - transform return Uint8Array", async () => { + const transformer = testTransformer({ + "foo.txt": "foo", + }); + + transformer.onTransform({ pluginName: "foo", filter: /.*/ }, () => { + return { + content: new TextEncoder().encode("foobar"), + }; + }); + + const result = await transformer.process("foo.txt", "development", ""); + const files = await consumeResult(result!); + expect(files).toEqual([ + { content: "foobar", map: null, path: "foo.txt", inputFiles: ["foo.txt"] }, + ]); +}); + +Deno.test("FileTransformer - pass transformed content", async () => { + const transformer = testTransformer({ + "input.txt": "input", + }); + + transformer.onTransform({ pluginName: "A", filter: /.*/ }, (args) => { + return { + content: args.text + " -> A", + }; + }); + transformer.onTransform({ pluginName: "B", filter: /.*/ }, (args) => { + return { + content: args.text + " -> B", + }; + }); + + const result = await transformer.process("input.txt", "development", ""); + const files = await consumeResult(result!); + expect(files).toEqual([ + { + content: "input -> A -> B", + map: null, + path: "input.txt", + inputFiles: ["input.txt"], + }, + ]); +}); + +Deno.test( + "FileTransformer - pass transformed content with multiple", + async () => { + const transformer = testTransformer({ + "input.txt": "input", + }); + + transformer.onTransform({ pluginName: "A", filter: /.*/ }, (args) => { + return [{ + path: args.path, + content: args.text + " -> A", + }]; + }); + transformer.onTransform({ pluginName: "B", filter: /.*/ }, (args) => { + return { + content: args.text + " -> B", + }; + }); + + const result = await transformer.process("input.txt", "development", ""); + const files = await consumeResult(result!); + expect(files).toEqual([ + { + content: "input -> A -> B", + map: null, + path: "input.txt", + inputFiles: ["input.txt"], + }, + ]); + }, +); + +Deno.test("FileTransformer - return multiple results", async () => { + const transformer = testTransformer({ + "foo.txt": "foo", + }); + + const received: string[] = []; + transformer.onTransform({ pluginName: "A", filter: /foo\.txt$/ }, () => { + return [{ + path: "a.txt", + content: "A", + }, { + path: "b.txt", + content: "B", + }]; + }); + transformer.onTransform({ pluginName: "B", filter: /.*/ }, (args) => { + received.push(args.path); + }); + + const result = await transformer.process("foo.txt", "development", ""); + const files = await consumeResult(result!); + expect(files).toEqual([ + { content: "A", map: null, path: "a.txt", inputFiles: ["foo.txt"] }, + { content: "B", map: null, path: "b.txt", inputFiles: ["foo.txt"] }, + ]); + expect(received).toEqual(["foo.txt", "b.txt", "a.txt"]); +}); + +Deno.test( + "FileTransformer - track input files through temporary results", + async () => { + const transformer = testTransformer({ + "foo.txt": "foo", + }); + + transformer.onTransform({ pluginName: "A", filter: /foo\.txt$/ }, () => { + return [{ + path: "a.txt", + content: "A", + }, { + path: "b.txt", + content: "B", + }]; + }); + transformer.onTransform( + { pluginName: "B", filter: /[ab]\.txt$/ }, + (args) => { + return { + path: "c" + args.path, + content: args.text + "C", + }; + }, + ); + + const result = await transformer.process("foo.txt", "development", ""); + const files = await consumeResult(result!); + expect(files).toEqual([ + { content: "AC", map: null, path: "ca.txt", inputFiles: ["foo.txt"] }, + { content: "BC", map: null, path: "cb.txt", inputFiles: ["foo.txt"] }, + ]); + }, +); diff --git a/src/dev/imports.ts b/src/dev/imports.ts deleted file mode 100644 index 0e6f4e3d5ce..00000000000 --- a/src/dev/imports.ts +++ /dev/null @@ -1,39 +0,0 @@ -export const RECOMMENDED_PREACT_VERSION = "10.22.0"; -export const RECOMMENDED_PREACT_SIGNALS_VERSION = "1.2.2"; -export const RECOMMENDED_PREACT_SIGNALS_CORE_VERSION = "1.5.1"; -export const RECOMMENDED_TWIND_CORE_VERSION = "1.1.3"; -export const RECOMMENDED_TWIND_PRESET_AUTOPREFIX_VERSION = "1.0.7"; -export const RECOMMENDED_TWIND_PRESET_TAILWIND_VERSION = "1.1.4"; -export const RECOMMENDED_STD_VERSION = "0.216.0"; -export const RECOMMENDED_TAILIWIND_VERSION = "3.4.1"; - -export function freshImports(imports: Record) { - imports["$fresh/"] = new URL("../../", import.meta.url).href; - imports["preact"] = `https://esm.sh/preact@${RECOMMENDED_PREACT_VERSION}`; - imports["preact/"] = `https://esm.sh/preact@${RECOMMENDED_PREACT_VERSION}/`; - imports["@preact/signals"] = - `https://esm.sh/*@preact/signals@${RECOMMENDED_PREACT_SIGNALS_VERSION}`; - imports["@preact/signals-core"] = - `https://esm.sh/*@preact/signals-core@${RECOMMENDED_PREACT_SIGNALS_CORE_VERSION}`; -} - -export function twindImports(imports: Record) { - imports["@twind/core"] = - `https://esm.sh/@twind/core@${RECOMMENDED_TWIND_CORE_VERSION}`; - imports["@twind/preset-tailwind"] = - `https://esm.sh/@twind/preset-tailwind@${RECOMMENDED_TWIND_PRESET_TAILWIND_VERSION}/`; - imports["@twind/preset-autoprefix"] = - `https://esm.sh/@twind/preset-autoprefix@${RECOMMENDED_TWIND_PRESET_AUTOPREFIX_VERSION}/`; -} - -export function tailwindImports(imports: Record) { - imports["tailwindcss"] = `npm:tailwindcss@${RECOMMENDED_TAILIWIND_VERSION}`; - imports["tailwindcss/"] = - `npm:/tailwindcss@${RECOMMENDED_TAILIWIND_VERSION}/`; - imports["tailwindcss/plugin"] = - `npm:/tailwindcss@${RECOMMENDED_TAILIWIND_VERSION}/plugin.js`; -} - -export function dotenvImports(imports: Record) { - imports["$std/"] = `https://deno.land/std@${RECOMMENDED_STD_VERSION}/`; -} diff --git a/src/dev/manifest.ts b/src/dev/manifest.ts deleted file mode 100644 index 51f2f40550a..00000000000 --- a/src/dev/manifest.ts +++ /dev/null @@ -1,134 +0,0 @@ -import { stringToIdentifier } from "../server/init_safe_deps.ts"; -import { extname, join, normalize } from "./deps.ts"; - -/** - * Import specifiers must have forward slashes - */ -function toImportSpecifier(file: string) { - let specifier = normalize(file).replace(/\\/g, "/"); - if (!specifier.startsWith(".")) { - specifier = "./" + specifier; - } - return specifier; -} - -// Create a valid JS identifier out of the project relative specifier. -// Note that we only need to deal with strings that _must_ have been -// valid file names in Windows, macOS and Linux and every identifier we -// create here will be prefixed with at least one "$". This greatly -// simplifies the invalid characters we have to account for. -export function specifierToIdentifier(specifier: string, used: Set) { - specifier = specifier.replace(/^(?:\.\/routes|\.\/islands)\//, ""); - const ext = extname(specifier); - if (ext) specifier = specifier.slice(0, -ext.length); - - // Turn the specifier into a readable JS identifier - let ident = stringToIdentifier(specifier); - - if (used.has(ident)) { - let check = ident; - let i = 1; - while (used.has(check)) { - check = `${ident}_${i++}`; - } - ident = check; - } - - used.add(ident); - return ident; -} - -export interface Manifest { - routes: string[]; - islands: string[]; -} - -export async function generate(directory: string, manifest: Manifest) { - const { routes, islands } = manifest; - - // Keep track of which identifier we've already used - const used = new Set(); - - const normalizedRoutes = new Map(); - for (let i = 0; i < routes.length; i++) { - const file = routes[i]; - const specifier = toImportSpecifier(file); - const identifier = specifierToIdentifier(specifier, used); - normalizedRoutes.set(specifier, identifier); - } - - const normalizedIslands: { specifier: string; identifier: string }[] = []; - for (let i = 0; i < islands.length; i++) { - const file = islands[i]; - const specifier = toImportSpecifier(file); - const identifier = specifierToIdentifier(specifier, used); - normalizedIslands.push({ specifier, identifier }); - } - - const output = `// DO NOT EDIT. This file is generated by Fresh. -// This file SHOULD be checked into source version control. -// This file is automatically updated during development when running \`dev.ts\`. - -${ - Array.from(normalizedRoutes.entries()).map(([specifier, identifier]) => - `import * as $${identifier} from "${specifier}";` - ).join( - "\n", - ) - } -${ - normalizedIslands.map(({ specifier, identifier }) => - `import * as $${identifier} from "${specifier}";` - ) - .join("\n") - } -import type { Manifest } from "$fresh/server.ts"; - -const manifest = { - routes: { - ${ - Array.from(normalizedRoutes.entries()).map(([specifier, identifier]) => - `${JSON.stringify(`${specifier}`)}: $${identifier},` - ) - .join("\n ") - } - }, - islands: { - ${ - normalizedIslands.map(({ specifier, identifier }) => - `${JSON.stringify(`${specifier}`)}: $${identifier},` - ) - .join("\n ") - } - }, - baseUrl: import.meta.url, -} satisfies Manifest; - -export default manifest; -`; - - const proc = new Deno.Command(Deno.execPath(), { - args: ["fmt", "-"], - stdin: "piped", - stdout: "piped", - stderr: "null", - }).spawn(); - - const raw = new ReadableStream({ - start(controller) { - controller.enqueue(new TextEncoder().encode(output)); - controller.close(); - }, - }); - await raw.pipeTo(proc.stdin); - const { stdout } = await proc.output(); - - const manifestStr = new TextDecoder().decode(stdout); - const manifestPath = join(directory, "./fresh.gen.ts"); - - await Deno.writeTextFile(manifestPath, manifestStr); - console.log( - `%cThe manifest has been generated for ${routes.length} routes and ${islands.length} islands.`, - "color: blue; font-weight: bold", - ); -} diff --git a/src/dev/manifest_test.ts b/src/dev/manifest_test.ts deleted file mode 100644 index b428dd3674b..00000000000 --- a/src/dev/manifest_test.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { assertEquals } from "./deps.ts"; -import { specifierToIdentifier } from "./manifest.ts"; - -const run = specifierToIdentifier; - -Deno.test("specifierToIdentifier", () => { - const used = new Set(); - assertEquals(run("foo/bar.ts", used), "foo_bar"); - assertEquals(run("foo/bar.json.ts", used), "foo_bar_json"); - assertEquals(run("foo/[id]/bar", used), "foo_id_bar"); - assertEquals(run("foo/[...all]/bar", used), "foo_all_bar"); - assertEquals(run("foo/[[optional]]/bar", used), "foo_optional_bar"); - assertEquals(run("foo/as-df/bar", used), "foo_as_df_bar"); - assertEquals(run("foo/as@df", used), "foo_as_df"); - assertEquals(run("foo/foo.bar.baz.tsx", used), "foo_foo_bar_baz"); - assertEquals(run("404", used), "_404"); - assertEquals(run("foo/_middleware", used), "foo_middleware"); -}); - -Deno.test("specifierToIdentifier deals with duplicates", () => { - const used = new Set(); - assertEquals(run("foo/bar", used), "foo_bar"); - assertEquals(run("foo/bar", used), "foo_bar_1"); -}); diff --git a/src/server/code_frame.ts b/src/dev/middlewares/error_overlay/code_frame.tsx similarity index 83% rename from src/server/code_frame.ts rename to src/dev/middlewares/error_overlay/code_frame.tsx index 6827dafcd05..3a54cf20372 100644 --- a/src/server/code_frame.ts +++ b/src/dev/middlewares/error_overlay/code_frame.tsx @@ -1,4 +1,5 @@ -import { colors, fromFileUrl } from "./deps.ts"; +import * as path from "@std/path"; +import * as colors from "@std/fmt/colors"; function tabs2Spaces(str: string) { return str.replace(/^\t+/, (tabs) => " ".repeat(tabs.length)); @@ -85,7 +86,10 @@ export interface StackFrame { line: number; column: number; } -export function getFirstUserFile(stack: string): StackFrame | undefined { +function getFirstUserFile( + stack: string, + rootDir: string, +): StackFrame | undefined { const lines = stack.split("\n"); for (let i = 0; i < lines.length; i++) { const match = lines[i].match(STACK_FRAME); @@ -96,6 +100,11 @@ export function getFirstUserFile(stack: string): StackFrame | undefined { const column = +match[4]; if (file.startsWith("file://")) { + const filePath = path.fromFileUrl(file); + if (path.relative(rootDir, filePath).startsWith(".")) { + continue; + } + return { fnName, file, @@ -107,20 +116,18 @@ export function getFirstUserFile(stack: string): StackFrame | undefined { } } -export async function getCodeFrame(error: Error) { - if (!error.stack) return; - - const file = getFirstUserFile(error.stack); +export function getCodeFrame(stack: string, rootDir: string) { + const file = getFirstUserFile(stack, rootDir); if (file) { try { - const filePath = fromFileUrl(file.file); - const text = await Deno.readTextFile(filePath); + const filePath = path.fromFileUrl(file.file); + const text = Deno.readTextFileSync(filePath); return createCodeFrame( text, file.line - 1, file.column - 1, ); - } catch { + } catch (err) { // Ignore } } diff --git a/src/dev/middlewares/error_overlay/middleware.tsx b/src/dev/middlewares/error_overlay/middleware.tsx new file mode 100644 index 00000000000..e74554b5412 --- /dev/null +++ b/src/dev/middlewares/error_overlay/middleware.tsx @@ -0,0 +1,32 @@ +import { DEV_ERROR_OVERLAY_URL } from "../../../constants.ts"; +import { HttpError } from "../../../error.ts"; +import type { MiddlewareFn } from "../../../middlewares/mod.ts"; +import { FreshScripts } from "../../../runtime/server/preact_hooks.tsx"; +import { ErrorOverlay } from "./overlay.tsx"; + +export function devErrorOverlay(): MiddlewareFn { + return async (ctx) => { + const { config, url } = ctx; + if (url.pathname === config.basePath + DEV_ERROR_OVERLAY_URL) { + return ctx.render(); + } + + try { + return await ctx.next(); + } catch (err) { + if (ctx.req.headers.get("accept")?.includes("text/html")) { + let init: ResponseInit | undefined; + if (err instanceof HttpError) { + if (err.status < 500) throw err; + init = { status: err.status }; + } + + // At this point we're pretty sure to have a server error + console.error(err); + + return ctx.render(, init); + } + throw err; + } + }; +} diff --git a/src/dev/middlewares/error_overlay/middleware_test.ts b/src/dev/middlewares/error_overlay/middleware_test.ts new file mode 100644 index 00000000000..f2b49fcc950 --- /dev/null +++ b/src/dev/middlewares/error_overlay/middleware_test.ts @@ -0,0 +1,57 @@ +import { expect } from "@std/expect"; +import { App } from "../../../app.ts"; +import { FakeServer } from "../../../test_utils.ts"; +import { devErrorOverlay } from "./middleware.tsx"; +import { HttpError } from "../../../error.ts"; + +Deno.test("error overlay - show when error is thrown", async () => { + const app = new App(); + app.use(devErrorOverlay()); + app.config.mode = "development"; + + app.get("/", () => { + throw new Error("fail"); + }); + + const server = new FakeServer(await app.handler()); + const res = await server.get("/", { + headers: { + accept: "text/html", + }, + }); + const content = await res.text(); + expect(content).toContain("fresh-error-overlay"); +}); + +Deno.test("error overlay - should not be visible for HttpError <500", async () => { + const app = new App(); + app.use(devErrorOverlay()); + app.config.mode = "development"; + + app + .get("/", () => { + throw new HttpError(404); + }) + .get("/500", () => { + throw new HttpError(500); + }); + + const server = new FakeServer(await app.handler()); + let res = await server.get("/", { + headers: { + accept: "text/html", + }, + }); + let content = await res.text(); + expect(content).not.toContain("fresh-error-overlay"); + expect(res.status).toEqual(404); + + res = await server.get("/500", { + headers: { + accept: "text/html", + }, + }); + content = await res.text(); + expect(content).toContain("fresh-error-overlay"); + expect(res.status).toEqual(500); +}); diff --git a/src/server/error_overlay.tsx b/src/dev/middlewares/error_overlay/overlay.tsx similarity index 91% rename from src/server/error_overlay.tsx rename to src/dev/middlewares/error_overlay/overlay.tsx index a46da4a3709..e6c317df992 100644 --- a/src/server/error_overlay.tsx +++ b/src/dev/middlewares/error_overlay/overlay.tsx @@ -1,6 +1,4 @@ -import { ComponentChildren, h } from "preact"; -import { render } from "./render.ts"; -import { PageProps } from "../server/mod.ts"; +import type { ComponentChildren } from "preact"; // Just to get some syntax highlighting const css = (arr: TemplateStringsArray, ...exts: never[]) => { @@ -118,12 +116,18 @@ function CodeFrame(props: { codeFrame: string }) { ); } -export function ErrorOverlay(props: PageProps) { +const DEFAULT_MESSAGE = "Internal Server Error"; + +export function ErrorOverlay(props: { url: URL }) { const url = props.url; - const title = url.searchParams.get("message") || "Internal Server Error"; + const title = url.searchParams.get("message") || DEFAULT_MESSAGE; const stack = url.searchParams.get("stack"); const codeFrame = url.searchParams.get("code-frame"); + if (title === DEFAULT_MESSAGE && !stack && !codeFrame) { + return null; + } + return ( <>
diff --git a/src/dev/middlewares/live_reload.ts b/src/dev/middlewares/live_reload.ts new file mode 100644 index 00000000000..8e182e420d4 --- /dev/null +++ b/src/dev/middlewares/live_reload.ts @@ -0,0 +1,39 @@ +import type { MiddlewareFn } from "../../middlewares/mod.ts"; +import { ALIVE_URL } from "../../constants.ts"; + +// Live reload: Send updates to browser +export function liveReload(): MiddlewareFn { + const revision = Date.now(); + + return (ctx) => { + const { config, req, url } = ctx; + + const aliveUrl = config.basePath + ALIVE_URL; + + if (url.pathname === aliveUrl) { + if (req.headers.get("upgrade") !== "websocket") { + return new Response(null, { status: 501 }); + } + + // TODO: When a change is made the Deno server restarts, + // so for now the WebSocket connection is only used for + // the client to know when the server is back up. Once we + // have HMR we'll actively start sending messages back + // and forth. + const { response, socket } = Deno.upgradeWebSocket(req); + + socket.addEventListener("open", () => { + socket.send( + JSON.stringify({ + type: "initial-state", + revision, + }), + ); + }); + + return response; + } + + return ctx.next(); + }; +} diff --git a/src/dev/mod.ts b/src/dev/mod.ts index d2d47bb3624..7046d68823a 100644 --- a/src/dev/mod.ts +++ b/src/dev/mod.ts @@ -1,111 +1,10 @@ -import { - greaterOrEqual, - join, - relative, - semverParse, - walk, - WalkEntry, -} from "./deps.ts"; -export { generate, type Manifest } from "./manifest.ts"; -import { generate, type Manifest } from "./manifest.ts"; -import { error } from "./error.ts"; -const MIN_DENO_VERSION = "1.31.0"; -const TEST_FILE_PATTERN = /[._]test\.(?:[tj]sx?|[mc][tj]s)$/; +import { setMode } from "../runtime/server/mod.tsx"; -export function ensureMinDenoVersion() { - // Check that the minimum supported Deno version is being used. - if ( - !greaterOrEqual( - semverParse(Deno.version.deno), - semverParse(MIN_DENO_VERSION), - ) - ) { - let message = - `Deno version ${MIN_DENO_VERSION} or higher is required. Please update Deno.\n\n`; +export { Builder, type FreshBuilder } from "./builder.ts"; +export { + type OnTransformArgs, + type OnTransformOptions, + type TransformFn, +} from "./file_transformer.ts"; - 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); - } -} - -async function collectDir( - dir: string, - callback: (entry: WalkEntry, dir: string) => void, - ignoreFilePattern = TEST_FILE_PATTERN, -): Promise { - // Check if provided path is a directory - try { - const stat = await Deno.stat(dir); - if (!stat.isDirectory) return; - } catch (err) { - if (err instanceof Deno.errors.NotFound) return; - throw err; - } - - const routesFolder = walk(dir, { - includeDirs: false, - includeFiles: true, - exts: ["tsx", "jsx", "ts", "js"], - skip: [ignoreFilePattern], - }); - - for await (const entry of routesFolder) { - callback(entry, dir); - } -} - -const GROUP_REG = /[/\\\\]\((_[^/\\\\]+)\)[/\\\\]/; -export async function collect( - directory: string, - ignoreFilePattern?: RegExp, -): Promise { - const filePaths = new Set(); - - const routes: string[] = []; - const islands: string[] = []; - await Promise.all([ - collectDir(join(directory, "./routes"), (entry, dir) => { - const rel = join("routes", relative(dir, entry.path)); - const normalized = rel.slice(0, rel.lastIndexOf(".")); - - // A `(_islands)` path segment is a local island folder. - // Any route path segment wrapped in `(_...)` is ignored - // during route collection. - const match = normalized.match(GROUP_REG); - if (match && match[1].startsWith("_")) { - if (match[1] === "_islands") { - islands.push(rel); - } - return; - } - - if (filePaths.has(normalized)) { - throw new Error( - `Route conflict detected. Multiple files have the same name: ${dir}${normalized}`, - ); - } - filePaths.add(normalized); - routes.push(rel); - }, ignoreFilePattern), - collectDir(join(directory, "./islands"), (entry, dir) => { - const rel = join("islands", relative(dir, entry.path)); - islands.push(rel); - }, ignoreFilePattern), - ]); - - routes.sort(); - islands.sort(); - - return { routes, islands }; -} - -export async function manifest(path: string, ignoreFilePattern?: RegExp) { - const manifest = await collect(path, ignoreFilePattern); - await generate(path, manifest); -} +setMode("development"); diff --git a/src/dev/update_check.ts b/src/dev/update_check.ts index a20cf23e78e..4d6596296c5 100644 --- a/src/dev/update_check.ts +++ b/src/dev/update_check.ts @@ -1,4 +1,6 @@ -import { colors, join, lessThan, semverParse } from "./deps.ts"; +import * as semver from "@std/semver"; +import * as colors from "@std/fmt/colors"; +import * as path from "@std/path"; export interface CheckFile { last_checked: string; @@ -33,7 +35,7 @@ function getHomeDir(): string | null { function getFreshCacheDir(): string | null { const home = getHomeDir(); - if (home) return join(home, "fresh"); + if (home) return path.join(home, "fresh"); return null; } @@ -47,10 +49,9 @@ async function fetchLatestVersion() { } async function readCurrentVersion() { - const versions = (await import("../../versions.json", { + return (await import("../../deno.json", { with: { type: "json" }, - })).default as string[]; - return versions[0]; + })).default.version; } export async function updateCheck( @@ -70,7 +71,7 @@ export async function updateCheck( const home = getCacheDir(); if (!home) return; - const filePath = join(home, "latest.json"); + const filePath = path.join(home, "latest.json"); try { await Deno.mkdir(home, { recursive: true }); } catch (err) { @@ -113,12 +114,12 @@ export async function updateCheck( } // Only show update message if current version is smaller than latest - const currentVersion = semverParse(checkFile.current_version); - const latestVersion = semverParse(checkFile.latest_version); + const currentVersion = semver.parse(checkFile.current_version); + const latestVersion = semver.parse(checkFile.latest_version); if ( (!checkFile.last_shown || Date.now() >= new Date(checkFile.last_shown).getTime() + interval) && - lessThan(currentVersion, latestVersion) + semver.lessThan(currentVersion, latestVersion) ) { checkFile.last_shown = new Date().toISOString(); diff --git a/src/dev/update_check_test.ts b/src/dev/update_check_test.ts new file mode 100644 index 00000000000..a4de6b6dbde --- /dev/null +++ b/src/dev/update_check_test.ts @@ -0,0 +1,394 @@ +import * as path from "@std/path"; +import denoJson from "../../deno.json" with { type: "json" }; +import { WEEK } from "@std/datetime"; +import { getStdOutput } from "../../tests/test_utils.tsx"; +import { expect } from "@std/expect"; +import type { CheckFile } from "./update_check.ts"; + +const CURRENT_VERSION = denoJson.version; + +const cwd = import.meta.dirname!; + +Deno.test("stores update check file in $HOME/fresh", async () => { + const tmpDirName = await Deno.makeTempDir(); + const filePath = path.join(tmpDirName, "latest.json"); + + await new Deno.Command(Deno.execPath(), { + args: [ + "run", + "-A", + path.join(cwd, "../../tests/fixture_update_check/mod.ts"), + ], + cwd, + env: { + CI: "false", + TEST_HOME: tmpDirName, + }, + }).output(); + + const text = JSON.parse(await Deno.readTextFile(filePath)); + expect(text).toEqual({ + current_version: CURRENT_VERSION, + latest_version: "99.99.999", + last_checked: text.last_checked, + last_shown: text.last_shown, + }); + + await Deno.remove(tmpDirName, { recursive: true }); +}); + +Deno.test("skips update check on specific environment variables", async (t) => { + const envs = ["FRESH_NO_UPDATE_CHECK", "CI", "DENO_DEPLOYMENT_ID"]; + + for (const env of envs) { + await t.step(`checking ${env}`, async () => { + const tmpDirName = await Deno.makeTempDir(); + const out = await new Deno.Command(Deno.execPath(), { + args: [ + "run", + "-A", + path.join(cwd, "../../tests/fixture_update_check/mod.ts"), + ], + cwd, + env: { + [env]: "true", + TEST_HOME: tmpDirName, + LATEST_VERSION: "1.30.0", + }, + stderr: "piped", + stdout: "piped", + }).output(); + + const { stdout } = getStdOutput(out); + expect(stdout).not.toMatch(/Fresh 1\.30\.0 is available/); + + await Deno.remove(tmpDirName, { recursive: true }); + }); + } +}); + +Deno.test("shows update message on version mismatch", async () => { + const tmpDirName = await Deno.makeTempDir(); + const filePath = path.join(tmpDirName, "latest.json"); + + await Deno.writeTextFile( + filePath, + JSON.stringify({ + current_version: "1.1.0", + latest_version: "1.1.0", + last_checked: new Date(0).toISOString(), + }), + ); + + const out = await new Deno.Command(Deno.execPath(), { + args: [ + "run", + "-A", + path.join(cwd, "../../tests/fixture_update_check/mod.ts"), + ], + cwd, + env: { + CI: "false", + TEST_HOME: tmpDirName, + LATEST_VERSION: "999.999.0", + }, + stderr: "piped", + stdout: "piped", + }).output(); + + const { stdout } = getStdOutput(out); + expect(stdout).toMatch(/Fresh 999\.999\.0 is available/); + + // Updates check file + const text = JSON.parse(await Deno.readTextFile(filePath)); + expect(text).toEqual({ + current_version: CURRENT_VERSION, + latest_version: "999.999.0", + last_checked: text.last_checked, + last_shown: text.last_shown, + }); + + await Deno.remove(tmpDirName, { recursive: true }); +}); + +Deno.test("only fetch new version defined by interval", async (t) => { + const tmpDirName = await Deno.makeTempDir(); + + await t.step("fetches latest version initially", async () => { + const out = await new Deno.Command(Deno.execPath(), { + args: [ + "run", + "-A", + path.join(cwd, "../../tests/fixture_update_check/mod.ts"), + ], + cwd, + env: { + CI: "false", + UPDATE_INTERVAL: "100000", + TEST_HOME: tmpDirName, + LATEST_VERSION: "1.30.0", + }, + stderr: "piped", + stdout: "piped", + }).output(); + + const { stdout } = getStdOutput(out); + expect(stdout).toMatch(/fetching latest version/); + }); + + await t.step("should not fetch if interval has not passed", async () => { + const out = await new Deno.Command(Deno.execPath(), { + args: [ + "run", + "-A", + path.join(cwd, "../../tests/fixture_update_check/mod.ts"), + ], + cwd, + env: { + CI: "false", + UPDATE_INTERVAL: "100000", + TEST_HOME: tmpDirName, + LATEST_VERSION: "1.30.0", + }, + stderr: "piped", + stdout: "piped", + }).output(); + + const { stdout } = getStdOutput(out); + expect(stdout).not.toMatch(/fetching latest version/); + }); + + await t.step("fetches if interval has passed", async () => { + const out = await new Deno.Command(Deno.execPath(), { + args: [ + "run", + "-A", + path.join(cwd, "../../tests/fixture_update_check/mod.ts"), + ], + cwd, + env: { + CI: "false", + UPDATE_INTERVAL: "1 ", + TEST_HOME: tmpDirName, + LATEST_VERSION: "1.30.0", + }, + }).output(); + + const { stdout } = getStdOutput(out); + expect(stdout).toMatch(/fetching latest version/); + }); + + await Deno.remove(tmpDirName, { recursive: true }); +}); + +Deno.test("updates current version in cache file", async () => { + const tmpDirName = await Deno.makeTempDir(); + + const checkFile: CheckFile = { + current_version: "1.2.0", + latest_version: "1.2.0", + last_checked: new Date(Date.now() - WEEK).toISOString(), + }; + + await Deno.writeTextFile( + path.join(tmpDirName, "latest.json"), + JSON.stringify(checkFile, null, 2), + ); + + const out = await new Deno.Command(Deno.execPath(), { + args: [ + "run", + "-A", + path.join(cwd, "../../tests/fixture_update_check/mod.ts"), + ], + cwd, + env: { + CI: "false", + TEST_HOME: tmpDirName, + LATEST_VERSION: CURRENT_VERSION, + }, + stderr: "piped", + stdout: "piped", + }).output(); + + const { stdout } = getStdOutput(out); + expect(stdout).not.toMatch(/Fresh .* is available/); + + await Deno.remove(tmpDirName, { recursive: true }); +}); + +Deno.test("only shows update message when current < latest", async () => { + const tmpDirName = await Deno.makeTempDir(); + + const checkFile: CheckFile = { + current_version: "9999.999.0", + latest_version: "1.2.0", + last_checked: new Date().toISOString(), + }; + + await Deno.writeTextFile( + path.join(tmpDirName, "latest.json"), + JSON.stringify(checkFile, null, 2), + ); + + const out = await new Deno.Command(Deno.execPath(), { + args: [ + "run", + "-A", + path.join(cwd, "../../tests/fixture_update_check/mod.ts"), + ], + cwd, + env: { + CI: "false", + TEST_HOME: tmpDirName, + LATEST_VERSION: CURRENT_VERSION, + CURRENT_VERSION: "99999.9999.0", + }, + stderr: "piped", + stdout: "piped", + }).output(); + + const { stdout } = getStdOutput(out); + expect(stdout).not.toMatch(/Fresh .* is available/); + + await Deno.remove(tmpDirName, { recursive: true }); +}); + +Deno.test("migrates to last_shown property", async () => { + const tmpDirName = await Deno.makeTempDir(); + + const checkFile: CheckFile = { + latest_version: "1.4.0", + current_version: "1.2.0", + last_checked: new Date().toISOString(), + }; + + await Deno.writeTextFile( + path.join(tmpDirName, "latest.json"), + JSON.stringify(checkFile, null, 2), + ); + + const out = await new Deno.Command(Deno.execPath(), { + args: [ + "run", + "-A", + path.join(cwd, "../../tests/fixture_update_check/mod.ts"), + ], + cwd, + env: { + CI: "false", + TEST_HOME: tmpDirName, + CURRENT_VERSION: "1.2.0", + LATEST_VERSION: "99999.9999.0", + }, + stderr: "piped", + stdout: "piped", + }).output(); + + const { stdout } = getStdOutput(out); + expect(stdout).toMatch(/Fresh .* is available/); + + const checkFileAfter = JSON.parse( + await Deno.readTextFile( + path.join(tmpDirName, "latest.json"), + ), + ); + + // Check if last version was written + expect(typeof checkFileAfter.last_shown).toEqual("string"); + + await Deno.remove(tmpDirName, { recursive: true }); +}); + +Deno.test("doesn't show update if last_shown + interval >= today", async () => { + const tmpDirName = await Deno.makeTempDir(); + + const todayMinus1Hour = new Date(); + todayMinus1Hour.setHours(todayMinus1Hour.getHours() - 1); + + const checkFile: CheckFile = { + current_version: "1.2.0", + latest_version: "1.6.0", + last_checked: new Date().toISOString(), + last_shown: todayMinus1Hour.toISOString(), + }; + + await Deno.writeTextFile( + path.join(tmpDirName, "latest.json"), + JSON.stringify(checkFile, null, 2), + ); + + const out = await new Deno.Command(Deno.execPath(), { + args: [ + "run", + "-A", + path.join(cwd, "../../tests/fixture_update_check/mod.ts"), + ], + cwd, + env: { + CI: "false", + TEST_HOME: tmpDirName, + CURRENT_VERSION: "1.2.0", + LATEST_VERSION: "99999.9999.0", + }, + stderr: "piped", + stdout: "piped", + }).output(); + + const { stdout } = getStdOutput(out); + expect(stdout).not.toMatch(/Fresh .* is available/); + + await Deno.remove(tmpDirName, { recursive: true }); +}); + +Deno.test( + "shows update if last_shown + interval < today", + async () => { + const tmpDirName = await Deno.makeTempDir(); + + const yesterday = new Date(); + yesterday.setDate(yesterday.getDate() - 1); + + const checkFile: CheckFile = { + current_version: "1.2.0", + latest_version: "99.999.99", + last_checked: new Date().toISOString(), + last_shown: yesterday.toISOString(), + }; + + await Deno.writeTextFile( + path.join(tmpDirName, "latest.json"), + JSON.stringify(checkFile, null, 2), + ); + + const out = await new Deno.Command(Deno.execPath(), { + args: [ + "run", + "-A", + path.join(cwd, "../../tests/fixture_update_check/mod.ts"), + ], + cwd, + env: { + CI: "false", + TEST_HOME: tmpDirName, + CURRENT_VERSION: CURRENT_VERSION, + LATEST_VERSION: "99999.9999.0", + }, + stderr: "piped", + stdout: "piped", + }).output(); + + const { stdout } = getStdOutput(out); + expect(stdout).toMatch(/Fresh .* is available/); + + const checkFileAfter = JSON.parse( + await Deno.readTextFile( + path.join(tmpDirName, "latest.json"), + ), + ); + + expect(checkFileAfter.last_shown).not.toEqual(yesterday.toISOString()); + + await Deno.remove(tmpDirName, { recursive: true }); + }, +); diff --git a/src/error.ts b/src/error.ts new file mode 100644 index 00000000000..e6e2c149531 --- /dev/null +++ b/src/error.ts @@ -0,0 +1,155 @@ +import { MODE } from "./runtime/server/mod.tsx"; + +export function getMessage(status: number): string { + switch (status) { + case 100: + return "Continue"; + case 101: + return "Switching Protocols"; + case 102: + return "Processing (WebDAV)"; + case 103: + return "Early Hints"; + case 200: + return "OK"; + case 201: + return "Created"; + case 202: + return "Accepted"; + case 203: + return "Non-Authoritative Information"; + case 204: + return "No Content"; + case 205: + return "Reset Content"; + case 206: + return "Partial Content"; + case 207: + return "Multi-Status (WebDAV)"; + case 208: + return "Already Reported (WebDAV)"; + case 226: + return "IM Used (HTTP Delta encoding)"; + case 300: + return "Multiple Choices"; + case 301: + return "Moved Permanently"; + case 302: + return "Found"; + case 303: + return "See Other"; + case 304: + return "Not Modified"; + case 305: + return "Use Proxy Deprecated"; + case 306: + return "unused"; + case 307: + return "Temporary Redirect"; + case 308: + return "Permanent Redirect"; + case 400: + return "Bad Request"; + case 401: + return "Unauthorized"; + case 402: + return "Payment Required Experimental"; + case 403: + return "Forbidden"; + case 404: + return "Not Found"; + case 405: + return "Method Not Allowed"; + case 406: + return "Not Acceptable"; + case 407: + return "Proxy Authentication Required"; + case 408: + return "Request Timeout"; + case 409: + return "Conflict"; + case 410: + return "Gone"; + case 411: + return "Length Required"; + case 412: + return "Precondition Failed"; + case 413: + return "Payload Too Large"; + case 414: + return "URI Too Long"; + case 415: + return "Unsupported Media Type"; + case 416: + return "Range Not Satisfiable"; + case 417: + return "Expectation Failed"; + case 418: + return "I'm a teapot"; + case 421: + return "Misdirected Request"; + case 422: + return "Unprocessable Content (WebDAV)"; + case 423: + return "Locked (WebDAV)"; + case 424: + return "Failed Dependency (WebDAV)"; + case 425: + return "Too Early Experimental"; + case 426: + return "Upgrade Required"; + case 428: + return "Precondition Required"; + case 429: + return "Too Many Requests"; + case 431: + return "Request Header Fields Too Large"; + case 451: + return "Unavailable For Legal Reasons"; + case 500: + return "Internal Server Error"; + case 501: + return "Not Implemented"; + case 502: + return "Bad Gateway"; + case 503: + return "Service Unavailable"; + case 504: + return "Gateway Timeout"; + case 505: + return "HTTP Version Not Supported"; + case 506: + return "Variant Also Negotiates"; + case 507: + return "Insufficient Storage (WebDAV)"; + case 508: + return "Loop Detected (WebDAV)"; + case 510: + return "Not Extended"; + case 511: + return "Network Authentication Required"; + default: + return "Internal Server Error"; + } +} + +export class HttpError { + #error: Error | null = null; + name = "HttpError"; + message: string; + + constructor( + public status: number, + message: string = getMessage(status), + public options?: ErrorOptions, + ) { + this.message = message; + if (MODE !== "production") { + this.#error = new Error(); + } + } + + get stack(): string | undefined { + return this.#error?.stack; + } +} diff --git a/src/error_test.ts b/src/error_test.ts new file mode 100644 index 00000000000..da86e97b832 --- /dev/null +++ b/src/error_test.ts @@ -0,0 +1,27 @@ +import { expect } from "@std/expect"; +import { MODE, setMode } from "./runtime/server/mod.tsx"; +import { HttpError } from "./error.ts"; + +Deno.test("HttpError - contains stack in development", () => { + const tmp = MODE; + setMode("development"); + try { + const err = new HttpError(404); + expect(err.status).toEqual(404); + expect(typeof err.stack).toEqual("string"); + } finally { + setMode(tmp); + } +}); + +Deno.test("HttpError - contains no stack in production", () => { + const tmp = MODE; + setMode("production"); + try { + const err = new HttpError(404); + expect(err.status).toEqual(404); + expect(err.stack).toEqual(undefined); + } finally { + setMode(tmp); + } +}); diff --git a/src/finish_setup.tsx b/src/finish_setup.tsx new file mode 100644 index 00000000000..b3673d22059 --- /dev/null +++ b/src/finish_setup.tsx @@ -0,0 +1,54 @@ +import type { ComponentChildren } from "preact"; + +export function FinishSetup() { + return ( + +
+
+

Finish setting up Fresh

+
    +
  1. + Go to your project in Deno Deploy and click the{" "} + Settings tab. +
  2. +
  3. + In the Git Integration section, enter deno task build + {" "} + in the Build Command input. +
  4. +
  5. + Save the changes. +
  6. +
+
+
+
+ ); +} + +export function ForgotBuild() { + return ( + +
+
+

Missing build directory

+

+ Did you forget to run deno task build? +

+
+
+
+ ); +} + +function Doc(props: { children?: ComponentChildren }) { + return ( + + + + Finish setting up Fresh + + {props.children} + + ); +} diff --git a/src/fs.ts b/src/fs.ts new file mode 100644 index 00000000000..35c4d149ec5 --- /dev/null +++ b/src/fs.ts @@ -0,0 +1,39 @@ +import { walk, type WalkEntry, type WalkOptions } from "@std/fs/walk"; + +export interface FreshFile { + size: number; + readable: ReadableStream; +} + +export interface FsAdapter { + walk( + root: string | URL, + options?: WalkOptions, + ): AsyncIterableIterator; + isDirectory(path: string | URL): Promise; + mkdirp(dir: string): Promise; + readFile(path: string | URL): Promise; +} + +export const fsAdapter: FsAdapter = { + walk, + async isDirectory(path) { + try { + const stat = await Deno.stat(path); + return stat.isDirectory; + } catch (err) { + if (err instanceof Deno.errors.NotFound) return false; + throw err; + } + }, + async mkdirp(dir: string) { + try { + await Deno.mkdir(dir, { recursive: true }); + } catch (err) { + if (!(err instanceof Deno.errors.AlreadyExists)) { + throw err; + } + } + }, + readFile: Deno.readFile, +}; diff --git a/src/handlers.ts b/src/handlers.ts new file mode 100644 index 00000000000..662d46c807b --- /dev/null +++ b/src/handlers.ts @@ -0,0 +1,172 @@ +import type { FreshContext } from "./context.ts"; +import type { Method } from "./router.ts"; + +export interface Render { + data: T; + headers?: HeadersInit; + status?: number; +} + +/** + * A handler function that can be used to specify how a given route should + * handle requests. + * + * The handler function can either return a {@link Response} object, or some + * data that can be rendered by a page component. See {@link HandlerFn} for more + * information. + * + * ### Per method handlers + * + * A route handler can be specific to a given HTTP method (GET, POST, PUT, + * DELETE, etc). To define a method-specific handler, specify an object that + * maps method names to functions that conform to the {@link HandlerFn} + * signature. + * + * ```ts + * export const handlers = defineHandlers({ + * GET: (ctx) => { + * return new Response("Hello from a GET request!"); + * }, + * POST: (ctx) => { + * return new Response("Hello from a POST request!"); + * } + * }); + * ``` + * + * Any requests to methods not specified in the handler object will result in a + * 405 Method Not Allowed response. If you want to handle these requests, you + * can define a catch-all handler. + * + * If a GET handler is specified, but no HEAD handler is specified, a HEAD + * handler will automatically be generated that calls the GET handler and + * strips the response body. + * + * ### Catch-all handlers + * + * A route handler can also catch all requests in a route. To define a catch-all + * handler, specify a function that conforms to the {@link HandlerFn} signature. + * This function will be called for all requests, regardless of the method. + * + * ```ts + * export const handlers = defineHandlers((ctx) => { + * return new Response(`Hello from a ${ctx.req.method} request!`); + * }); + * ``` + */ +export type RouteHandler = + | HandlerFn + | HandlerByMethod; + +export function isHandlerByMethod( + handler: RouteHandler, +): handler is HandlerByMethod { + return handler !== null && typeof handler === "object"; +} + +/** + * A handler function that is invoked when a request is made to a route. The + * handler function is passed a {@link FreshContext} object that contains the + * original request object, as well as any state related to the current request. + * + * The handler function can either return a {@link Response} object, which will + * be sent back to the client, or some data that will be passed to the routes' + * page component for rendering. + * + * ### Returning a Response + * + * If the handler function returns a {@link Response} object, the response will + * be sent back to the client. This can be used to send back static content, or + * to redirect the client to another URL. + * + * ```ts + * export const handler = defineHandler((ctx) => { + * return new Response("Hello, world!"); + * }); + * ``` + * + * ### Returning data + * + * If the handler function returns an object with a `data` property, the data + * will be passed to the page component, where it can be rendered into HTML. + * + * ```ts + * export const handler = defineHandler((ctx) => { + * return { data: { message: "Hello, world!" } }; + * }); + * + * export default definePage(({ data }) => { + * return

{data.message}

; + * }); + * ``` + * + * When returning data, you can also specify additional properties that will be + * used when constructing the response object from the HTML generated by the + * page component. For example, you can specify custom headers, a custom status + * code, or a list of elements to include in the ``. + * + * ```tsx + * export const handler = defineHandler((ctx) => { + * return { + * data: { message: "Hello, world!" }, + * headers: { "Cache-Control": "public, max-age=3600" }, + * status: 201, + * head: [Hello, world!], + * }; + * }); + * ``` + * + * ### Asynchronous handlers + * + * The handler function can also be asynchronous. This can be useful if you need + * to fetch data from an external source, or perform some other asynchronous + * operation before returning a response. + * + * ```ts + * export const handler = defineHandler(async (ctx) => { + * const resp = await fetch("https://api.example.com/data").; + * if (!resp.ok) { + * throw new Error("Failed to fetch data"); + * } + * const data = await resp.json(); + * return { data }; + * }); + * ``` + * + * If you initiate multiple asynchronous operations in a handler, you can use + * `Promise.all` to wait for all of them to complete at the same time. This can + * speed up the response time of your handler, as it allows you to perform + * multiple operations concurrently. + * + * ```ts + * export const handler = defineHandler(async (ctx) => { + * const [resp1, resp2] = await Promise.all([ + * fetch("https://api.example.com/data1") + * .then((resp) => resp.json()), + * fetch("https://api.example.com/data2") + * .then((resp) => resp.json()), + * ]); + * return { data: { data1, data2 } }; + * }); + * ``` + */ +export interface HandlerFn { + (ctx: FreshContext): + | Response + | Render + | void + | Promise | void>; +} + +/** + * A set of handler functions that routes requests based on the HTTP method. + * + * See {@link RouteHandler} for more information on how to use this type. + */ +export type HandlerByMethod = { + [M in Method]?: HandlerFn; +}; + +export type RouteData< + Handler extends RouteHandler, +> = Handler extends (RouteHandler) ? Data + : never; diff --git a/src/helpers.ts b/src/helpers.ts new file mode 100644 index 00000000000..111e327d263 --- /dev/null +++ b/src/helpers.ts @@ -0,0 +1,155 @@ +import type { AnyComponent } from "preact"; +import type { HandlerByMethod, RouteHandler } from "./handlers.ts"; +import type { FreshContext } from "./context.ts"; +import type { Middleware } from "./middlewares/mod.ts"; + +/** + * A set of helper functions that enable better type inference and code + * completion when defining routes and middleware. + * + * To create a helpers object, call {@link createHelpers}. + */ +export interface Helpers { + /** + * Define a {@link RouteHandler} object. This function returns the passed + * input as-is. + * + * You can use this function to help the TypeScript compiler infer the types + * of your route handlers. For example: + * + * ```ts + * export const handler = helpers.defineHandlers((ctx) => { + * ctx.url; // ctx is inferred to be a FreshContext object, so this is a URL + * return new Response("Hello, world!"); + * }); + * ``` + * + * This is particularly useful when combined with the {@link definePage} + * helper function, which can infer the data type from the handler function. + * For more information, see {@link definePage}. + * + * You can also pass an explicit type argument to ensure that all data + * returned from the render function is of the correct type: + * + * ```ts + * export const handler = helpers.defineHandlers<{ slug: string }>({ + * async GET(ctx) { + * const slug = ctx.params.slug; // slug is inferred to be a string + * return { data: { slug } }; + * }, + * + * // This method will cause a type error because the data object is missing + * // the required `slug` property. + * async POST(ctx) { + * return { data: { } }; + * }, + * }); + * ``` + * + * @typeParam Data The type of data that the handler returns. This will be inferred from the handler methods if not provided. + * @typeParam Handlers This will always be inferred from the input object. Do not manually specify this type. + */ + defineHandlers< + Data, + Handlers extends RouteHandler = RouteHandler, + >( + handlers: Handlers, + ): typeof handlers; + + /** + * Define a page component that will be rendered when a route handler returns + * data. This function returns the passed input as-is. + * + * You can use this function to help the TypeScript compiler infer the types + * of the data that your page component receives. For example: + * + * ```ts + * export default helpers.definePage((props) => { + * const slug = props.params.slug; // Because props is inferred to be a FreshContext object, slug is inferred to be a string + * return

{slug}

; + * }); + * ``` + * + * This is particularly useful when combined with the {@link defineHandlers} + * helper function, in which case the data type will be inferred from the + * return type of the handler method. + * + * ```ts + * export const handler = defineHandlers({ + * async GET(ctx) { + * const slug = ctx.params.slug; // slug is inferred to be a string + * return { data: { slug } }; + * }, + * }); + * + * export default definePage(({ data }) => { + * const slug = data.slug; // slug is inferred to be a string here + * return

{slug}

; + * }); + * ``` + * + * As a rule of thumb, always use this function to define your page + * components. If you also have a handler for this route, pass the handler + * object as a type argument to this function. If you do not have a handler, + * omit the type argument. + * + * @typeParam Handler The type of the handler object that this page component is associated with. If this route has a handler, pass the handler object as a type argument to this function, e.g. `typeof handler`. If this route does not have a handler, omit this type argument. + * @typeParam Data The type of data that the page component receives. This will be inferred from the handler methods if not provided. In very advanced use cases, you can specify `never` to the `Handler` type argument and provide the `Data` type explicitly. + */ + definePage< + Handler extends RouteHandler = never, + Data = Handler extends HandlerByMethod ? Data : never, + >( + render: AnyComponent & { Component: () => null }>, + ): typeof render; + + /** + * Define a {@link Middleware} that will be used to process requests before + * they are passed to the route handler. This function returns the passed + * input as-is. + * + * You can use this function to help the TypeScript compiler infer the types + * of the context object that your middleware receives. For example: + * + * ```ts + * export const middleware = helpers.defineMiddleware((ctx) => { + * ctx.url; // ctx is inferred to be a FreshContext object, so this is a URL + * return ctx.next(); + * }); + * ``` + * + * You may also pass an array of middleware functions to this function. + * + * @typeParam M The type of the middleware function. This will be inferred from the input function. Do not manually specify this type. + */ + defineMiddleware>( + middleware: M, + ): typeof middleware; +} + +/** + * Create a set of helper functions that enable better type inference and code + * completion when defining routes and middleware. + * + * To use, call this function in a central file and export the result. In your + * route and middleware files, import the {@link Helpers|helpers object} and use + * them to define your routes and middleware using the + * {@link Helpers.defineHandlers|defineHandlers}, + * {@link Helpers.definePage|definePage}, and + * {@link Helpers.defineMiddleware|defineMiddleware} functions. + * + * @typeParam State The type of the state object that is passed to all middleware and route handlers. + */ +export function createHelpers(): Helpers { + return { + defineHandlers(handlers) { + return handlers; + }, + definePage(render) { + return render; + }, + defineMiddleware(middleware) { + return middleware; + }, + }; +} diff --git a/src/jsonify/constants.ts b/src/jsonify/constants.ts new file mode 100644 index 00000000000..07a273c7ef0 --- /dev/null +++ b/src/jsonify/constants.ts @@ -0,0 +1,7 @@ +export const UNDEFINED = -1; +export const NULL = -2; +export const NAN = -3; +export const INFINITY_POS = -4; +export const INFINITY_NEG = -5; +export const ZERO_NEG = -6; +export const HOLE = -7; diff --git a/src/jsonify/custom_test.ts b/src/jsonify/custom_test.ts new file mode 100644 index 00000000000..0aa5ffef32d --- /dev/null +++ b/src/jsonify/custom_test.ts @@ -0,0 +1,54 @@ +import { expect } from "@std/expect"; +import { parse } from "./parse.ts"; +import { stringify } from "./stringify.ts"; +import { Signal, signal } from "@preact/signals"; + +Deno.test("custom parse - Point", () => { + class Point { + constructor(public x: number, public y: number) { + this.x = x; + this.y = y; + } + } + + const str = stringify(new Point(30, 40), { + Point: (value) => value instanceof Point ? [value.x, value.y] : undefined, + }); + + expect(str).toEqual('[["Point",1],[2,3],30,40]'); + + const point = parse(str, { + Point: ([x, y]: [number, number]) => new Point(x, y), + }); + expect(point).toEqual(new Point(30, 40)); +}); + +Deno.test("custom parse - Signals", () => { + const res = parse('[["Signal",1],2]', { + Signal: (value) => signal(value), + }); + expect(res).toBeInstanceOf(Signal); + expect(res.peek()).toEqual(2); +}); + +Deno.test("custom stringify - Signals", () => { + const s = signal(2); + expect(stringify(s, { + Signal: (s2: unknown) => { + return s2 instanceof Signal ? s2.peek() : undefined; + }, + })).toEqual( + '[["Signal",1],2]', + ); +}); + +Deno.test("custom stringify - referenced Signals", () => { + const s = signal(2); + expect(stringify([s, s], { + Signal: (s2: unknown) => { + return s2 instanceof Signal ? s2.peek() : undefined; + }, + })).toEqual( + '[[1,1],["Signal",2],2]', + ); +}); diff --git a/src/jsonify/parse.ts b/src/jsonify/parse.ts new file mode 100644 index 00000000000..66ed83ca89c --- /dev/null +++ b/src/jsonify/parse.ts @@ -0,0 +1,193 @@ +import { + HOLE, + INFINITY_NEG, + INFINITY_POS, + NAN, + NULL, + UNDEFINED, + ZERO_NEG, +} from "./constants.ts"; + +// deno-lint-ignore no-explicit-any +export type CustomParser = Record unknown>; + +export function parse( + value: string, + custom?: CustomParser | undefined, +): T { + const data = JSON.parse(value); + + const hydrated = new Array(data.length); + // deno-lint-ignore no-explicit-any + unpack(data, hydrated, 0, custom) as any; + return hydrated[0]; +} + +function unpack( + arr: unknown[], + hydrated: unknown[], + idx: number, + custom: CustomParser | undefined, +): void { + if (idx in hydrated) return; + + const current = arr[idx]; + if (typeof current === "number") { + switch (current) { + case UNDEFINED: + hydrated[idx] = undefined; + return; + case NULL: + hydrated[idx] = null; + return; + case NAN: + hydrated[idx] = NaN; + return; + case INFINITY_POS: + hydrated[idx] = Infinity; + return; + case INFINITY_NEG: + hydrated[idx] = -Infinity; + return; + case ZERO_NEG: + hydrated[idx] = -0; + return; + default: + hydrated[idx] = current; + return; + } + } else if ( + typeof current === "string" || typeof current === "boolean" || + current === null + ) { + hydrated[idx] = current; + return; + } else if (Array.isArray(current)) { + if (current.length > 0 && typeof current[0] === "string") { + const name = current[0]; + if (custom !== undefined && name in custom) { + const fn = custom[name]; + const ref = current[1]; + unpack(arr, hydrated, ref, custom); + const value = hydrated[ref]; + hydrated[idx] = fn(value); + return; + } + switch (name) { + case "BigInt": + hydrated[idx] = BigInt(current[1]); + return; + case "Date": + hydrated[idx] = new Date(current[1]); + return; + case "RegExp": + hydrated[idx] = new RegExp(current[1], current[2]); + return; + case "Set": { + const set = new Set(); + for (let i = 0; i < current[1].length; i++) { + const ref = current[1][i]; + unpack(arr, hydrated, ref, custom); + set.add(hydrated[ref]); + } + hydrated[idx] = set; + return; + } + case "Map": { + const set = new Map(); + for (let i = 0; i < current[1].length; i++) { + const refKey = current[1][i++]; + unpack(arr, hydrated, refKey, custom); + const refValue = current[1][i]; + unpack(arr, hydrated, refValue, custom); + + set.set(hydrated[refKey], hydrated[refValue]); + } + hydrated[idx] = set; + return; + } + case "Uint8Array": + hydrated[idx] = b64decode(current[1]); + return; + } + } else { + const actual = new Array(current.length); + hydrated[idx] = actual; + for (let i = 0; i < current.length; i++) { + const ref = current[i]; + if (ref < 0) { + switch (ref) { + case UNDEFINED: + actual[i] = undefined; + break; + case NULL: + actual[i] = null; + break; + case NAN: + actual[i] = NaN; + break; + case INFINITY_POS: + actual[i] = Infinity; + break; + case INFINITY_NEG: + actual[i] = -Infinity; + break; + case ZERO_NEG: + actual[i] = -0; + break; + case HOLE: + continue; + } + } else { + unpack(arr, hydrated, ref, custom); + actual[i] = hydrated[ref]; + } + } + } + } else if (typeof current === "object") { + const actual: Record = {}; + hydrated[idx] = actual; + + const keys = Object.keys(current); + for (let i = 0; i < keys.length; i++) { + const key = keys[i]; + // deno-lint-ignore no-explicit-any + const ref = (current as any)[key]; + if (ref < 0) { + switch (ref) { + case UNDEFINED: + actual[key] = undefined; + break; + case NULL: + actual[key] = null; + break; + case NAN: + actual[key] = NaN; + break; + case INFINITY_POS: + actual[key] = Infinity; + break; + case INFINITY_NEG: + actual[key] = -Infinity; + break; + case ZERO_NEG: + actual[key] = -0; + break; + } + } else { + unpack(arr, hydrated, ref, custom); + actual[key] = hydrated[ref]; + } + } + } +} + +function b64decode(b64: string): Uint8Array { + const binString = atob(b64); + const size = binString.length; + const bytes = new Uint8Array(size); + for (let i = 0; i < size; i++) { + bytes[i] = binString.charCodeAt(i); + } + return bytes; +} diff --git a/src/jsonify/parse_test.ts b/src/jsonify/parse_test.ts new file mode 100644 index 00000000000..8e5a1b88df1 --- /dev/null +++ b/src/jsonify/parse_test.ts @@ -0,0 +1,99 @@ +import { expect } from "@std/expect"; +import { parse } from "./parse.ts"; + +Deno.test("parse - json", () => { + expect(parse("[2]")).toEqual(2); + expect(parse('["abc"]')).toEqual("abc"); + expect(parse("[true]")).toEqual(true); + expect(parse("[false]")).toEqual(false); + expect(parse("[[1,2,3],1,2,3]")).toEqual([1, 2, 3]); + expect(parse('[{"a":1,"b":-2,"c":2},1,[-2]]')).toEqual({ + a: 1, + b: null, + c: [null], + }); +}); + +Deno.test("parse - hole array", () => { + expect(parse("[[1,-7,2],1,3]")).toEqual([1, , 3]); +}); + +Deno.test("parse - undefined", () => { + expect(parse("[-1]")).toEqual(undefined); +}); + +Deno.test("parse - Infinity", () => { + expect(parse("[-4]")).toEqual(Infinity); + expect(parse("[-5]")).toEqual(-Infinity); +}); + +Deno.test("parse - NaN", () => { + expect(parse("[-3]")).toEqual(NaN); +}); + +Deno.test("parse - -0", () => { + expect(parse("[-6]")).toEqual(-0); +}); + +Deno.test("parse - bigint", () => { + const n = BigInt(9007199254740991); + expect(parse('[["BigInt","9007199254740991"]]')).toEqual( + n, + ); +}); + +Deno.test("parse - Set", () => { + const res = parse('[["Set",[1,2]],1,{"foo":1}]'); + expect(res).toEqual( + new Set([1, { foo: 1 }]), + ); +}); + +Deno.test("parse - Map", () => { + expect(parse('[["Map",[1,2,3,4]],1,{"foo":1},2,3]')) + .toEqual( + new Map([[1, { foo: 1 }], [2, 3]]), + ); +}); + +Deno.test("parse - Date", () => { + const date = new Date("1990-05-31"); + expect(parse('[["Date","1990-05-31T00:00:00.000Z"]]')).toEqual( + date, + ); +}); + +Deno.test("parse - RegExp", () => { + let reg = /foo["]/; + expect(parse('[["RegExp","foo[\\"]", ""]]')).toEqual(reg); + + reg = /foo["]/g; + expect(parse('[["RegExp","foo[\\"]", "g"]]')).toEqual(reg); +}); + +Deno.test("parse - Uint8Array", () => { + const value = new Uint8Array([1, 2, 3]); + expect(parse('[["Uint8Array","AQID"]]')).toEqual( + value, + ); +}); + +Deno.test("parse - references", () => { + const inner = { foo: 123 }; + const obj = { a: inner, b: [inner, inner] }; + const res = parse('[{"a":1,"b":3},{"foo":2},123,[1,1]]'); + expect(res).toEqual(obj); + expect(res.a).toEqual(res.b[0]); + expect(res.a).toEqual(res.b[1]); +}); + +Deno.test("parse - circular references", () => { + // deno-lint-ignore no-explicit-any + const foo = { foo: null as any }; + foo.foo = foo; + expect(parse('[{"foo":0}]')).toEqual(foo); +}); + +Deno.test("parse - object", () => { + expect(parse('[{"foo":1},42]')).toEqual({ foo: 42 }); +}); diff --git a/src/jsonify/stringify.ts b/src/jsonify/stringify.ts new file mode 100644 index 00000000000..5511e8d713f --- /dev/null +++ b/src/jsonify/stringify.ts @@ -0,0 +1,186 @@ +import { + INFINITY_NEG, + INFINITY_POS, + NAN, + NULL, + UNDEFINED, + ZERO_NEG, +} from "./constants.ts"; +import { HOLE } from "./constants.ts"; + +// deno-lint-ignore no-explicit-any +export type Stringifiers = Record any>; + +/** + * Serializes the following: + * + * - `null` + * - `undefined` + * - `boolean` + * - `number` + * - `bigint` + * - `string` + * - `array` + * - `object` (no prototypes) + * - `Uint8Array` + * - `Date` + * - `RegExp` + * - `Set` + * - `Map` + * + * Circular references are supported and objects with the same reference are + * serialized only once. + */ +export function stringify(data: unknown, custom?: Stringifiers): string { + const out: string[] = []; + const indexes = new Map(); + const res = serializeInner(out, indexes, data, custom); + if (res < 0) { + out.push(String(res)); + } + return `[${out.join(",")}]`; +} + +function serializeInner( + out: string[], + indexes: Map, + value: unknown, + custom: Stringifiers | undefined, +): number { + const seenIdx = indexes.get(value); + if (seenIdx !== undefined) return seenIdx; + + if (value === undefined) return UNDEFINED; + if (value === null) return NULL; + if (Number.isNaN(value)) return NAN; + if (value === Infinity) return INFINITY_POS; + if (value === -Infinity) return INFINITY_NEG; + if (value === 0 && 1 / value < 0) return ZERO_NEG; + + const idx = out.length; + out.push(""); + indexes.set(value, idx); + + let str = ""; + + if (typeof value === "number") { + str += String(value); + } else if (typeof value === "boolean") { + str += String(value); + } else if (typeof value === "bigint") { + str += `["BigInt","${value}"]`; + } else if (typeof value === "string") { + str += JSON.stringify(value); + } else if (Array.isArray(value)) { + str += "["; + for (let i = 0; i < value.length; i++) { + if (i in value) { + str += serializeInner(out, indexes, value[i], custom); + } else { + str += HOLE; + } + + if (i < value.length - 1) { + str += ","; + } + } + str += "]"; + } else if (typeof value === "object") { + if (custom !== undefined) { + for (const k in custom) { + const fn = custom[k]; + if (fn === undefined) continue; + + const res = fn(value); + if (res === undefined) continue; + + serializeInner(out, indexes, res, custom); + str = `["${k}",${idx + 1}]`; + out[idx] = str; + return idx; + } + } + + if (value instanceof Date) { + str += `["Date","${value.toISOString()}"]`; + } else if (value instanceof RegExp) { + str += `["RegExp",${JSON.stringify(value.source)}, "${value.flags}"]`; + } else if (value instanceof Uint8Array) { + str += `["Uint8Array","${b64encode(value)}"]`; + } else if (value instanceof Set) { + const items = new Array(value.size); + let i = 0; + value.forEach((v) => { + items[i++] = serializeInner(out, indexes, v, custom); + }); + str += `["Set",[${items.join(",")}]]`; + } else if (value instanceof Map) { + const items = new Array(value.size * 2); + let i = 0; + value.forEach((v, k) => { + items[i++] = serializeInner(out, indexes, k, custom); + items[i++] = serializeInner(out, indexes, v, custom); + }); + str += `["Map",[${items.join(",")}]]`; + } else { + str += "{"; + const keys = Object.keys(value); + for (let i = 0; i < keys.length; i++) { + const key = keys[i]; + str += JSON.stringify(key) + ":"; + // deno-lint-ignore no-explicit-any + str += serializeInner(out, indexes, (value as any)[key], custom); + + if (i < keys.length - 1) { + str += ","; + } + } + str += "}"; + } + } else if (typeof value === "function") { + throw new Error(`Serializing functions is not supported.`); + } + + out[idx] = str; + return idx; +} + +// deno-fmt-ignore +const base64abc = [ + "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", + "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "a", "b", "c", "d", + "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", + "t", "u", "v", "w", "x", "y", "z", "0", "1", "2", "3", "4", "5", "6", "7", + "8", "9", "+", "/", +]; + +/** + * CREDIT: https://gist.github.com/enepomnyaschih/72c423f727d395eeaa09697058238727 + * Encodes a given Uint8Array, ArrayBuffer or string into RFC4648 base64 representation + */ +export function b64encode(buffer: ArrayBuffer): string { + const uint8 = new Uint8Array(buffer); + let result = "", + i; + const l = uint8.length; + for (i = 2; i < l; i += 3) { + result += base64abc[uint8[i - 2] >> 2]; + result += base64abc[((uint8[i - 2] & 0x03) << 4) | (uint8[i - 1] >> 4)]; + result += base64abc[((uint8[i - 1] & 0x0f) << 2) | (uint8[i] >> 6)]; + result += base64abc[uint8[i] & 0x3f]; + } + if (i === l + 1) { + // 1 octet yet to write + result += base64abc[uint8[i - 2] >> 2]; + result += base64abc[(uint8[i - 2] & 0x03) << 4]; + result += "=="; + } + if (i === l) { + // 2 octets yet to write + result += base64abc[uint8[i - 2] >> 2]; + result += base64abc[((uint8[i - 2] & 0x03) << 4) | (uint8[i - 1] >> 4)]; + result += base64abc[(uint8[i - 1] & 0x0f) << 2]; + result += "="; + } + return result; +} diff --git a/src/jsonify/stringify_test.ts b/src/jsonify/stringify_test.ts new file mode 100644 index 00000000000..912dda67380 --- /dev/null +++ b/src/jsonify/stringify_test.ts @@ -0,0 +1,109 @@ +import { expect } from "@std/expect"; +import { stringify } from "./stringify.ts"; + +Deno.test("stringify - json", () => { + expect(stringify(2)).toEqual("[2]"); + expect(stringify("abc")).toEqual('["abc"]'); + expect(stringify(true)).toEqual("[true]"); + expect(stringify(false)).toEqual("[false]"); + expect(stringify([1, 2, 3])).toEqual("[[1,2,3],1,2,3]"); + expect(stringify({ a: 1, b: null, c: [null] })).toEqual( + '[{"a":1,"b":-2,"c":2},1,[-2]]', + ); +}); + +Deno.test("stringify - hole array", () => { + expect(stringify([1, , 3])).toEqual("[[1,-7,2],1,3]"); +}); + +Deno.test("stringify - undefined", () => { + expect(stringify(undefined)).toEqual("[-1]"); +}); + +Deno.test("stringify - Infinity", () => { + expect(stringify(Infinity)).toEqual("[-4]"); + expect(stringify(-Infinity)).toEqual("[-5]"); +}); + +Deno.test("stringify - NaN", () => { + expect(stringify(NaN)).toEqual("[-3]"); +}); + +Deno.test("stringify - -0", () => { + expect(stringify(-0)).toEqual("[-6]"); +}); + +Deno.test("stringify - bigint", () => { + const n = BigInt(9007199254740991); + expect(stringify(n)).toEqual( + '[["BigInt","9007199254740991"]]', + ); +}); + +Deno.test("stringify - Date", () => { + const date = new Date("1990-05-31"); + expect(stringify(date)).toEqual( + '[["Date","1990-05-31T00:00:00.000Z"]]', + ); +}); + +Deno.test("stringify - RegExp", () => { + let reg = /foo["]/; + expect(stringify(reg)).toEqual( + '[["RegExp","foo[\\"]", ""]]', + ); + + reg = /foo["]/g; + expect(stringify(reg)).toEqual( + '[["RegExp","foo[\\"]", "g"]]', + ); +}); + +Deno.test("stringify - Set", () => { + expect(stringify(new Set([1, { foo: 1 }]))).toEqual( + '[["Set",[1,2]],1,{"foo":1}]', + ); +}); + +Deno.test("stringify - Map", () => { + expect(stringify(new Map([[1, { foo: 1 }], [2, 3]]))) + .toEqual( + '[["Map",[1,2,3,4]],1,{"foo":1},2,3]', + ); +}); + +Deno.test("stringify - Uint8Array", () => { + const value = new Uint8Array([1, 2, 3]); + expect(stringify(value)).toEqual( + '[["Uint8Array","AQID"]]', + ); +}); + +Deno.test("stringify - references", () => { + const inner = { foo: 123 }; + const obj = { a: inner, b: [inner, inner] }; + expect(stringify(obj)).toEqual( + '[{"a":1,"b":3},{"foo":2},123,[1,1]]', + ); +}); + +Deno.test("stringify - circular references", () => { + // deno-lint-ignore no-explicit-any + const foo = { foo: null as any }; + foo.foo = foo; + expect(stringify(foo)).toEqual( + '[{"foo":0}]', + ); +}); + +Deno.test("stringify - object prototype", () => { + const obj = { __proto__: 123, foo: 1 }; + expect(stringify(obj)).toEqual( + '[{"foo":1},1]', + ); +}); + +Deno.test("stringify - throw serializing functions", () => { + const fn = () => {}; + expect(() => stringify(fn)).toThrow(); +}); diff --git a/src/middlewares/mod.ts b/src/middlewares/mod.ts new file mode 100644 index 00000000000..a6b5028f14d --- /dev/null +++ b/src/middlewares/mod.ts @@ -0,0 +1,107 @@ +import type { FreshContext } from "../context.ts"; +import type { App as _App } from "../app.ts"; + +/** + * A middleware function is the basic building block of Fresh. It allows you + * to respond to an incoming request in any way you want. You can redirect + * routes, serve files, create APIs and much more. Middlewares can be chained by + * calling {@linkcode FreshContext.next|ctx.next()} inside of the function. + * + * Middlewares can be synchronous or asynchronous. If a middleware returns a + * {@linkcode Response} object, the response will be sent back to the client. If + * a middleware returns a `Promise`, Fresh will wait for the promise + * to resolve before sending the response. + * + * A {@linkcode FreshContext} object is passed to the middleware function. This + * object contains the original request object, as well as any state related to + * the current request. The context object also contains methods to redirect + * the client to another URL, or to call the next middleware in the chain. + * + * Middlewares can be defined as a single function or an array of functions. + * When an array of middlewares is passed to + * {@linkcode _App.prototype.use|app.use}, Fresh will call each middleware in the + * order they are defined. + * + * Middlewares can also be defined using the + * {@linkcode _App.prototype.defineMiddleware|app.defineMiddleware} method. This + * method is optional, but it can be useful for type checking and code + * completion. It does not register the middleware with the app. + * + * ## Examples + * + * ### Logging middleware + * + * This example shows how to create a simple middleware that logs incoming + * requests. + * + * ```ts + * // Define a middleware function that logs incoming requests. Using the + * // `defineMiddleware` method is optional, but it can be useful for type + * // checking and code completion. It does not register the middleware with the + * // app. + * const loggerMiddleware = app.defineMiddleware((ctx) => { + * console.log(`${ctx.req.method} ${ctx.req.url}`); + * // Call the next middleware + * return ctx.next(); + * }); + * + * // To register the middleware to the app, use `app.use`. + * app.use(loggerMiddleware) + * ``` + * + * ### Redirect middleware + * + * This example shows how to create a middleware that redirects requests from + * one URL to another. + * + * ```ts + * // Any request to a URL that starts with "/legacy/" will be redirected to + * // "/modern". + * const redirectMiddleware = app.defineMiddleware((ctx) => { + * if (ctx.url.pathname.startsWith("/legacy/")) { + * return ctx.redirect("/modern"); + * } + * + * // Otherwise call the next middleware + * return ctx.next(); + * }); + * + * // Again, register the middleware with the app. + * app.use(redirectMiddleware); + * ``` + */ +export type MiddlewareFn = ( + ctx: FreshContext, +) => Response | Promise; + +/** + * A single middleware function, or an array of middleware functions. For + * further information, see {@link MiddlewareFn}. + */ +export type Middleware = MiddlewareFn | MiddlewareFn[]; + +export function runMiddlewares( + middlewares: MiddlewareFn[][], + ctx: FreshContext, +): Promise { + let fn = ctx.next; + let i = middlewares.length; + while (i--) { + const stack = middlewares[i]; + let j = stack.length; + while (j--) { + const local = fn; + const next = stack[j]; + fn = async () => { + ctx.next = local; + try { + return await next(ctx); + } catch (err) { + ctx.error = err; + throw err; + } + }; + } + } + return fn(); +} diff --git a/src/middlewares/mod_test.ts b/src/middlewares/mod_test.ts new file mode 100644 index 00000000000..6176a6ff328 --- /dev/null +++ b/src/middlewares/mod_test.ts @@ -0,0 +1,136 @@ +import { runMiddlewares } from "./mod.ts"; +import { expect } from "@std/expect"; +import { serveMiddleware } from "../test_utils.ts"; +import type { MiddlewareFn } from "./mod.ts"; + +Deno.test("runMiddleware", async () => { + const middlewares: MiddlewareFn<{ text: string }>[] = [ + (ctx) => { + ctx.state.text = "A"; + return ctx.next(); + }, + (ctx) => { + ctx.state.text += "B"; + return ctx.next(); + }, + async (ctx) => { + const res = await ctx.next(); + ctx.state.text += "C"; // This should not show up + return res; + }, + (ctx) => { + return new Response(ctx.state.text); + }, + ]; + + const server = serveMiddleware<{ text: string }>((ctx) => + runMiddlewares([middlewares], ctx) + ); + + const res = await server.get("/"); + expect(await res.text()).toEqual("AB"); +}); + +Deno.test("runMiddleware - middlewares should only be called once", async () => { + const A: MiddlewareFn<{ count: number }> = (ctx) => { + if (ctx.state.count === undefined) { + ctx.state.count = 0; + } else { + ctx.state.count++; + } + return ctx.next(); + }; + + const server = serveMiddleware<{ count: number }>((ctx) => + runMiddlewares( + [[A, (ctx) => new Response(String(ctx.state.count))]], + ctx, + ) + ); + + const res = await server.get("/"); + expect(await res.text()).toEqual("0"); +}); + +Deno.test("runMiddleware - runs multiple stacks", async () => { + type State = { text: string }; + const A: MiddlewareFn = (ctx) => { + ctx.state.text += "A"; + return ctx.next(); + }; + const B: MiddlewareFn = (ctx) => { + ctx.state.text += "B"; + return ctx.next(); + }; + const C: MiddlewareFn = (ctx) => { + ctx.state.text += "C"; + return ctx.next(); + }; + const D: MiddlewareFn = (ctx) => { + ctx.state.text += "D"; + return ctx.next(); + }; + + const server = serveMiddleware((ctx) => { + ctx.state.text = ""; + return runMiddlewares( + [ + [A, B], + [C, D, (ctx) => new Response(String(ctx.state.text))], + ], + ctx, + ); + }); + + const res = await server.get("/"); + expect(await res.text()).toEqual("ABCD"); +}); + +Deno.test("runMiddleware - throws errors", async () => { + let thrownA: unknown = null; + let thrownB: unknown = null; + let thrownC: unknown = null; + + const middlewares: MiddlewareFn<{ text: string }>[] = [ + async (ctx) => { + try { + return await ctx.next(); + } catch (err) { + thrownA = err; + throw err; + } + }, + async (ctx) => { + try { + return await ctx.next(); + } catch (err) { + thrownB = err; + throw err; + } + }, + async (ctx) => { + try { + return await ctx.next(); + } catch (err) { + thrownC = err; + throw err; + } + }, + () => { + throw new Error("fail"); + }, + ]; + + const server = serveMiddleware<{ text: string }>((ctx) => + runMiddlewares([middlewares], ctx) + ); + + try { + await server.get("/"); + } catch { + // ignore + } + expect(thrownA).toBeInstanceOf(Error); + expect(thrownB).toBeInstanceOf(Error); + expect(thrownC).toBeInstanceOf(Error); +}); diff --git a/src/middlewares/static_files.ts b/src/middlewares/static_files.ts new file mode 100644 index 00000000000..63aa64a1f1f --- /dev/null +++ b/src/middlewares/static_files.ts @@ -0,0 +1,77 @@ +import * as path from "@std/path"; +import { contentType as getContentType } from "@std/media-types/content-type"; +import type { MiddlewareFn } from "@fresh/core"; +import { ASSET_CACHE_BUST_KEY } from "../runtime/shared_internal.tsx"; +import { BUILD_ID } from "../runtime/build_id.ts"; +import { getBuildCache } from "../context.ts"; + +/** + * Fresh middleware to enable file-system based routing. + * ```ts + * // Enable Fresh static file serving + * app.use(freshStaticFles()); + * ``` + */ +export function staticFiles(): MiddlewareFn { + return async function freshStaticFiles(ctx) { + const { req, url } = ctx; + const buildCache = getBuildCache(ctx); + + // Fast path bail out + const file = await buildCache.readFile(url.pathname); + if (url.pathname === "/" || file === null) { + // Optimization: Prevent long responses for favicon.ico requests + if (url.pathname === "/favicon.ico") { + return new Response(null, { status: 404 }); + } + return ctx.next(); + } + + if (req.method !== "GET" && req.method !== "HEAD") { + return new Response("Method Not Allowed", { status: 405 }); + } + + const cacheKey = url.searchParams.get(ASSET_CACHE_BUST_KEY); + if (cacheKey !== null && BUILD_ID !== cacheKey) { + url.searchParams.delete(ASSET_CACHE_BUST_KEY); + const location = url.pathname + url.search; + return new Response(null, { + status: 307, + headers: { + location, + }, + }); + } + + const ext = path.extname(url.pathname); + const etag = file.hash; + + const contentType = getContentType(ext); + const headers = new Headers({ + "Content-Type": contentType ?? "text/plain", + vary: "If-None-Match", + }); + + if (cacheKey === null || ctx.config.mode === "development") { + headers.append( + "Cache-Control", + "no-cache, no-store, max-age=0, must-revalidate", + ); + } else { + const ifNoneMatch = req.headers.get("If-None-Match"); + if ( + etag !== null && + (ifNoneMatch === etag || ifNoneMatch === "W/" + etag) + ) { + return new Response(null, { status: 304, headers }); + } + } + + headers.set("Content-Length", String(file.size)); + if (req.method === "HEAD") { + return new Response(null, { status: 200, headers }); + } + + return new Response(file.readable, { headers }); + }; +} diff --git a/src/middlewares/static_files_test.ts b/src/middlewares/static_files_test.ts new file mode 100644 index 00000000000..4e91286b9cc --- /dev/null +++ b/src/middlewares/static_files_test.ts @@ -0,0 +1,150 @@ +import { staticFiles } from "./static_files.ts"; +import { serveMiddleware } from "../test_utils.ts"; +import type { BuildCache, StaticFile } from "../build_cache.ts"; +import { expect } from "@std/expect"; +import { ASSET_CACHE_BUST_KEY } from "../runtime/shared_internal.tsx"; +import { BUILD_ID } from "../runtime/build_id.ts"; + +class MockBuildCache implements BuildCache { + buildId = "MockId"; + files = new Map(); + hasSnapshot = true; + + constructor(files: Record) { + const encoder = new TextEncoder(); + for (const [pathname, info] of Object.entries(files)) { + const text = encoder.encode(info.content); + + const normalized = pathname.startsWith("/") ? pathname : "/" + pathname; + this.files.set(normalized, { + hash: info.hash, + size: text.byteLength, + readable: text, + }); + } + } + + // deno-lint-ignore require-await + async readFile(pathname: string): Promise { + return this.files.get(pathname) ?? null; + } + getIslandChunkName(_islandName: string): string | null { + return null; + } +} + +Deno.test("static files - 200", async () => { + const buildCache = new MockBuildCache({ + "foo.css": { content: "body {}", hash: null }, + }); + const server = serveMiddleware( + staticFiles(), + { buildCache }, + ); + + const res = await server.get("/foo.css"); + const content = await res.text(); + expect(res.status).toEqual(200); + expect(res.headers.get("Content-Type")).toEqual("text/css; charset=UTF-8"); + expect(res.headers.get("Content-Length")).toEqual("7"); + expect(res.headers.get("Cache-Control")).toEqual( + "no-cache, no-store, max-age=0, must-revalidate", + ); + expect(content).toEqual("body {}"); +}); + +Deno.test("static files - HEAD 200", async () => { + const buildCache = new MockBuildCache({ + "foo.css": { content: "body {}", hash: null }, + }); + const server = serveMiddleware( + staticFiles(), + { buildCache }, + ); + + const res = await server.head("/foo.css"); + const content = await res.text(); + expect(res.status).toEqual(200); + expect(res.headers.get("Content-Type")).toEqual("text/css; charset=UTF-8"); + expect(res.headers.get("Content-Length")).toEqual("7"); + expect(content).toEqual(""); +}); + +Deno.test("static files - etag", async () => { + const buildCache = new MockBuildCache({ + "foo.css": { content: "body {}", hash: "123" }, + }); + const server = serveMiddleware( + staticFiles(), + { buildCache }, + ); + + const headers = new Headers(); + headers.append("If-None-Match", "123"); + const cacheUrl = `/foo.css?${ASSET_CACHE_BUST_KEY}=${BUILD_ID}`; + const res = await server.get(cacheUrl, { headers }); + await res.body?.cancel(); + expect(res.status).toEqual(304); + + const headers2 = new Headers(); + headers2.append("If-None-Match", "W/123"); + const res2 = await server.get(cacheUrl, { headers }); + await res2.body?.cancel(); + expect(res2.status).toEqual(304); +}); + +Deno.test("static files - 404 on missing favicon.ico", async () => { + const buildCache = new MockBuildCache({}); + const server = serveMiddleware( + staticFiles(), + { buildCache }, + ); + const res = await server.get("favicon.ico"); + await res.body?.cancel(); + expect(res.status).toEqual(404); +}); + +Deno.test("static files - 405 on wrong HTTP method", async () => { + const buildCache = new MockBuildCache({ + "foo.css": { content: "body {}", hash: null }, + }); + const server = serveMiddleware( + staticFiles(), + { buildCache }, + ); + + for (const method of ["post", "patch", "put", "delete"]) { + // deno-lint-ignore no-explicit-any + const res = await (server as any)[method]("/foo.css"); + await res.body?.cancel(); + expect(res.status).toEqual(405); + } +}); + +Deno.test("static files - disables caching in development", async () => { + const buildCache = new MockBuildCache({ + "foo.css": { content: "body {}", hash: null }, + }); + const server = serveMiddleware( + staticFiles(), + { + buildCache, + config: { + basePath: "", + build: { + outDir: "", + }, + mode: "development", + root: ".", + staticDir: "", + }, + }, + ); + + const res = await server.get("/foo.css"); + await res.body?.cancel(); + expect(res.status).toEqual(200); + expect(res.headers.get("Cache-Control")).toEqual( + "no-cache, no-store, max-age=0, must-revalidate", + ); +}); diff --git a/src/middlewares/trailing_slashes.ts b/src/middlewares/trailing_slashes.ts new file mode 100644 index 00000000000..98a004f1c17 --- /dev/null +++ b/src/middlewares/trailing_slashes.ts @@ -0,0 +1,29 @@ +import type { MiddlewareFn } from "./mod.ts"; + +/** + * Fresh middleware to force URLs to end with a slash or never end with one. + * + * ```ts + * // Always add trailing slash + * app.use(trailingSlashes("always")); + * // Never add trailing slashes to URLs and remove them if present + * app.use(trailingSlashes("never")); + * ``` + */ +export function trailingSlashes( + mode: "always" | "never", +): MiddlewareFn { + return function trailingSlashesMiddleware(ctx) { + const url = ctx.url; + if (url.pathname !== "/") { + if (mode === "always" && !url.pathname.endsWith("/")) { + return ctx.redirect(`${url.pathname}/${url.search}`); + } else if ( + mode === "never" && url.pathname.endsWith("/") + ) { + return ctx.redirect(`${url.pathname.slice(0, -1)}${url.search}`); + } + } + return ctx.next(); + }; +} diff --git a/src/middlewares/trailing_slashes_test.ts b/src/middlewares/trailing_slashes_test.ts new file mode 100644 index 00000000000..17a5d2c1c25 --- /dev/null +++ b/src/middlewares/trailing_slashes_test.ts @@ -0,0 +1,55 @@ +// deno-lint-ignore-file require-await +import { trailingSlashes } from "./trailing_slashes.ts"; +import { expect } from "@std/expect"; +import { serveMiddleware } from "../test_utils.ts"; + +Deno.test("trailingSlashes - always", async () => { + const middleware = trailingSlashes("always"); + const server = serveMiddleware(async (ctx) => { + ctx.next = async () => new Response("ok"); + return await middleware(ctx); + }); + + let res = await server.get("/"); + await res.body?.cancel(); + expect(res.status).toEqual(200); + expect(res.headers.get("Location")).toEqual(null); + + res = await server.get("/foo"); + await res.body?.cancel(); + expect(res.status).toEqual(302); + expect(res.headers.get("Location")).toEqual("/foo/"); + + res = await server.get("/foo/bar"); + await res.body?.cancel(); + expect(res.status).toEqual(302); + expect(res.headers.get("Location")).toEqual("/foo/bar/"); + + res = await server.get("/foo/bar/"); + await res.body?.cancel(); + expect(res.status).toEqual(200); + expect(res.headers.get("Location")).toEqual(null); +}); + +Deno.test("trailingSlashes - never", async () => { + const middleware = trailingSlashes("never"); + const server = serveMiddleware(async (ctx) => { + ctx.next = async () => new Response("ok"); + return await middleware(ctx); + }); + + let res = await server.get("/"); + await res.body?.cancel(); + expect(res.status).toEqual(200); + expect(res.headers.get("Location")).toEqual(null); + + res = await server.get("/foo"); + await res.body?.cancel(); + expect(res.status).toEqual(200); + expect(res.headers.get("Location")).toEqual(null); + + res = await server.get("/foo/"); + await res.body?.cancel(); + expect(res.status).toEqual(302); + expect(res.headers.get("Location")).toEqual("/foo"); +}); diff --git a/src/mod.ts b/src/mod.ts new file mode 100644 index 00000000000..e64f12d6026 --- /dev/null +++ b/src/mod.ts @@ -0,0 +1,33 @@ +export { App, type ListenOptions, type RouteCacheEntry } from "./app.ts"; +export { trailingSlashes } from "./middlewares/trailing_slashes.ts"; +export { fsRoutes, type FsRoutesOptions } from "./plugins/fs_routes/mod.ts"; +export { + type HandlerByMethod, + type HandlerFn, + type Render, + type RouteData, + type RouteHandler, +} from "./handlers.ts"; +export { type RouteConfig } from "./types.ts"; +export { type Middleware, type MiddlewareFn } from "./middlewares/mod.ts"; +export { staticFiles } from "./middlewares/static_files.ts"; +export { type Mode, type PageProps } from "./runtime/server/mod.tsx"; +export { type FreshConfig, type ResolvedFreshConfig } from "./config.ts"; +export { type FreshContext, type Island } from "./context.ts"; +export { createHelpers, type Helpers } from "./helpers.ts"; +export { type Method } from "./router.ts"; +export { HttpError } from "./error.ts"; + +// Compat with older Fresh versions +export { + type AppProps, + defineApp, + defineLayout, + defineRoute, + type ErrorPageProps, + type Handlers, + type Handlers as Handler, + type LayoutProps, + type RouteContext, + type UnknownPageProps, +} from "./compat/server.ts"; diff --git a/src/plugins/fs_routes/mod.ts b/src/plugins/fs_routes/mod.ts new file mode 100644 index 00000000000..19ea5ef6c24 --- /dev/null +++ b/src/plugins/fs_routes/mod.ts @@ -0,0 +1,450 @@ +import type { AnyComponent } from "preact"; +import type { App } from "../../app.ts"; +import type { WalkEntry } from "@std/fs/walk"; +import * as path from "@std/path"; +import type { RouteConfig } from "../../types.ts"; +import type { RouteHandler } from "../../handlers.ts"; +import type { FreshContext } from "../../context.ts"; +import type { MiddlewareFn } from "../../middlewares/mod.ts"; +import { + type AsyncAnyComponent, + renderMiddleware, +} from "./render_middleware.ts"; +import { type Method, pathToPattern } from "../../router.ts"; +import { type HandlerFn, isHandlerByMethod } from "../../handlers.ts"; +import { type FsAdapter, fsAdapter } from "../../fs.ts"; +import type { PageProps } from "../../runtime/server/mod.tsx"; +import { HttpError } from "../../error.ts"; +import { parseRootPath } from "../../config.ts"; + +const TEST_FILE_PATTERN = /[._]test\.(?:[tj]sx?|[mc][tj]s)$/; +const GROUP_REG = /(^|[/\\\\])\((_[^/\\\\]+)\)[/\\\\]/; + +interface InternalRoute { + path: string; + base: string; + filePath: string; + config: RouteConfig | null; + handlers: RouteHandler | null; + component: AnyComponent> | null; +} + +export interface FreshFsItem { + config?: RouteConfig; + handler?: RouteHandler | HandlerFn[]; + handlers?: RouteHandler; + default?: + | AnyComponent< + PageProps & Pick, "redirect"> + > + | AsyncAnyComponent< + PageProps & Pick, "redirect"> + >; +} + +// deno-lint-ignore no-explicit-any +function isFreshFile(mod: any): mod is FreshFsItem { + return mod !== null && typeof mod === "object" && + typeof mod.default === "function" || + typeof mod.config === "object" || typeof mod.handlers === "object" || + typeof mod.handlers === "function" || typeof mod.handler === "object" || + typeof mod.handler === "function"; +} + +export interface FsRoutesOptions { + dir?: string; + ignoreFilePattern?: RegExp[]; + loadRoute: (path: string) => Promise; + loadIsland: (path: string) => Promise; +} + +export interface TESTING_ONLY__FsRoutesOptions { + _fs?: FsAdapter; +} + +export async function fsRoutes( + app: App, + options_: FsRoutesOptions, +) { + const options = options_ as FsRoutesOptions & TESTING_ONLY__FsRoutesOptions; + const ignore = options.ignoreFilePattern ?? [TEST_FILE_PATTERN]; + const fs = options._fs ?? fsAdapter; + + const dir = options.dir ? parseRootPath(options.dir) : app.config.root; + const islandDir = path.join(dir, "islands"); + const routesDir = path.join(dir, "routes"); + + const islandPaths: string[] = []; + const relRoutePaths: string[] = []; + + // Walk routes folder + await Promise.all([ + walkDir( + islandDir, + (entry) => { + islandPaths.push(entry.path); + }, + ignore, + fs, + ), + walkDir( + routesDir, + (entry) => { + const relative = path.relative(routesDir, entry.path); + + // A `(_islands)` path segment is a local island folder. + // Any route path segment wrapped in `(_...)` is ignored + // during route collection. + const match = relative.match(GROUP_REG); + if (match && match[2][0] === "_") { + if (match[2] === "_islands") { + islandPaths.push(entry.path); + } + return; + } + + const url = new URL(relative, "http://localhost/"); + relRoutePaths.push(url.pathname.slice(1)); + }, + ignore, + fs, + ), + ]); + + await Promise.all(islandPaths.map(async (islandPath) => { + const relative = path.relative(islandDir, islandPath); + // deno-lint-ignore no-explicit-any + const mod = await options.loadIsland(relative) as any; + for (const key of Object.keys(mod)) { + const maybeFn = mod[key]; + if (typeof maybeFn === "function") { + app.island(islandPath, key, maybeFn); + } + } + })); + + const routeModules: InternalRoute[] = await Promise.all( + relRoutePaths.map(async (routePath) => { + const mod = await options.loadRoute(routePath); + if (!isFreshFile(mod)) { + throw new Error( + `Expected a route, middleware, layout or error template, but couldn't find relevant exports in: ${routePath}`, + ); + } + + const handlers = mod.handlers ?? mod.handler ?? null; + if (typeof handlers === "function" && handlers.length > 1) { + throw new Error( + `Handlers must only have one argument but found more than one. Check the function signature in: ${ + path.join(routesDir, routePath) + }`, + ); + } + + const normalizedPath = `/${ + routePath.slice(0, routePath.lastIndexOf(".")) + }`; + const base = normalizedPath.slice(0, normalizedPath.lastIndexOf("/")); + return { + path: normalizedPath, + filePath: routePath, + base, + handlers: mod.handlers ?? mod.handler ?? null, + config: mod.config ?? null, + component: mod.default ?? null, + } as InternalRoute; + }), + ); + + routeModules.sort((a, b) => sortRoutePaths(a.path, b.path)); + + const stack: InternalRoute[] = []; + let hasApp = false; + + for (let i = 0; i < routeModules.length; i++) { + const routeMod = routeModules[i]; + const normalized = routeMod.path; + + let j = stack.length - 1; + while ( + j >= 0 && stack[j].base !== "" && + !routeMod.path.startsWith(stack[j].base + "/") + ) { + j--; + stack.pop(); + } + + if (normalized.endsWith("/_app")) { + hasApp = true; + stack.push(routeMod); + continue; + } else if (normalized.endsWith("/_middleware")) { + stack.push(routeMod); + continue; + } else if (normalized.endsWith("/_layout")) { + stack.push(routeMod); + continue; + } else if (normalized.endsWith("/_error")) { + stack.push(routeMod); + continue; + } else if (normalized.endsWith("/_404")) { + stack.push(routeMod); + continue; + } else if (normalized.endsWith("/_500")) { + stack.push(routeMod); + continue; + } + + // Remove any elements not matching our parent path anymore + const middlewares: MiddlewareFn[] = []; + let components: AnyComponent>[] = []; + + let skipApp = !!routeMod.config?.skipAppWrapper; + const skipLayouts = !!routeMod.config?.skipInheritedLayouts; + + for (let k = 0; k < stack.length; k++) { + const mod = stack[k]; + if (mod.path.endsWith("/_middleware")) { + if (mod.handlers !== null && !isHandlerByMethod(mod.handlers)) { + middlewares.push(mod.handlers as MiddlewareFn); + } else if (Array.isArray(mod.handlers)) { + middlewares.push(...mod.handlers); + } + } + + // _app template + if (skipApp && mod.path === "/_app") { + hasApp = false; + continue; + } else if (!skipApp && mod.config?.skipAppWrapper) { + skipApp = true; + if (hasApp) { + hasApp = false; + // _app component is always first + components.shift(); + } + } + + // _layouts + if (skipLayouts && mod.path.endsWith("/_layout")) { + continue; + } else if (!skipLayouts && mod.config?.skipInheritedLayouts) { + const first = components.length > 0 ? components[0] : null; + components = []; + + if (!skipApp && hasApp && first !== null) { + components.push(first); + } + } + + if (mod.path.endsWith("/_error") || mod.path.endsWith("/_500")) { + const handlers = mod.handlers; + const handler = handlers === null || + (isHandlerByMethod(handlers) && Object.keys(handlers).length === 0) + ? undefined + : typeof handlers === "function" + ? handlers + : undefined; // FIXME: Method handler + const errorComponents = components.slice(); + if (mod.component !== null) { + errorComponents.push(mod.component); + } + let parent = mod.path.slice(0, -"_error".length); + parent = parent === "/" ? "*" : parent + "*"; + app.all( + parent, + errorMiddleware(errorComponents, handler), + ); + middlewares.push(errorMiddleware(errorComponents, handler)); + continue; + } + + if (mod.path.endsWith("/_404")) { + const handlers = mod.handlers; + const handler = handlers === null || + (isHandlerByMethod(handlers) && Object.keys(handlers).length === 0) + ? undefined + : typeof handlers === "function" + ? handlers + : undefined; // FIXME: Method handler + const notFoundComponents = components.slice(); + if (mod.component !== null) { + notFoundComponents.push(mod.component); + } + app.use(notFoundMiddleware(notFoundComponents, handler)); + continue; + } + + if (mod.component !== null) { + components.push(mod.component); + } + } + + if (routeMod.component !== null) { + components.push(routeMod.component); + } + + const handlers = routeMod.handlers; + const routePath = routeMod.config?.routeOverride ?? + pathToPattern(normalized.slice(1)); + + if ( + handlers === null || + (isHandlerByMethod(handlers) && Object.keys(handlers).length === 0) + ) { + const combined = middlewares.concat( + renderMiddleware(components, undefined), + ); + app.get(routePath, ...combined); + } else if (isHandlerByMethod(handlers)) { + for (const method of Object.keys(handlers) as Method[]) { + const fn = handlers[method]; + + if (fn !== undefined) { + const combined = middlewares.concat(renderMiddleware(components, fn)); + const lower = method.toLowerCase() as Lowercase; + app[lower](routePath, ...combined); + } + } + } else if (typeof handlers === "function") { + const combined = middlewares.concat( + renderMiddleware(components, handlers), + ); + app.all(routePath, ...combined); + } + } +} + +function errorMiddleware( + components: AnyComponent>[], + handler: HandlerFn | undefined, +): MiddlewareFn { + const mid = renderMiddleware(components, handler); + return async function errorMiddleware(ctx) { + try { + return await ctx.next(); + } catch (err) { + ctx.error = err; + return mid(ctx); + } + }; +} + +function notFoundMiddleware( + components: AnyComponent>[], + handler: HandlerFn | undefined, +): MiddlewareFn { + const mid = renderMiddleware(components, handler); + return async function notFoundMiddleware(ctx) { + try { + return await ctx.next(); + } catch (err) { + if (err instanceof HttpError && err.status === 404) { + return mid(ctx); + } + throw err; + } + }; +} + +async function walkDir( + dir: string, + callback: (entry: WalkEntry) => void, + ignore: RegExp[], + fs: FsAdapter, +) { + if (!await fs.isDirectory(dir)) return; + + const entries = fs.walk(dir, { + includeDirs: false, + includeFiles: true, + exts: ["tsx", "jsx", "ts", "js"], + skip: ignore, + }); + + for await (const entry of entries) { + callback(entry); + } +} + +const APP_REG = /_app(?!\.[tj]sx?)?$/; + +/** + * Sort route paths where special Fresh files like `_app`, + * `_layout` and `_middleware` are sorted in front. + */ +export function sortRoutePaths(a: string, b: string) { + // The `_app` route should always be the first + if (APP_REG.test(a)) return -1; + else if (APP_REG.test(b)) return 1; + + let segmentIdx = 0; + const aLen = a.length; + const bLen = b.length; + const maxLen = aLen > bLen ? aLen : bLen; + for (let i = 0; i < maxLen; i++) { + const charA = a.charAt(i); + const charB = b.charAt(i); + + if (charA === "/" || charB === "/") { + segmentIdx = i; + + // If the other path doesn't close the segment + // then we don't need to continue + if (charA !== "/") return 1; + if (charB !== "/") return -1; + + continue; + } + + if (i === segmentIdx + 1) { + const scoreA = getRoutePathScore(charA, a, i); + const scoreB = getRoutePathScore(charB, b, i); + if (scoreA === scoreB) { + if (charA !== charB) { + // TODO: Do we need localeSort here or is this good enough? + return charA < charB ? 0 : 1; + } + continue; + } + + return scoreA > scoreB ? -1 : 1; + } + + if (charA !== charB) { + // TODO: Do we need localeSort here or is this good enough? + return charA < charB ? 0 : 1; + } + } + + return 0; +} + +/** + * Assign a score based on the first two characters of a path segment. + * The goal is to sort `_middleware` and `_layout` in front of everything + * and `[` or `[...` last respectively. + */ +function getRoutePathScore(char: string, s: string, i: number): number { + if (char === "_") { + if (i + 1 < s.length) { + if (s[i + 1] === "e") return 6; + if (s[i + 1] === "m") return 5; + } + return 4; + } else if (char === "[") { + if (i + 1 < s.length && s[i + 1] === ".") { + return 0; + } + return 1; + } + + if ( + i + 4 === s.length - 1 && char === "i" && s[i + 1] === "n" && + s[i + 2] === "d" && s[i + 3] === "e" && s[i + 4] === "x" + ) { + return 3; + } + + return 2; +} diff --git a/src/plugins/fs_routes/mod_test.tsx b/src/plugins/fs_routes/mod_test.tsx new file mode 100644 index 00000000000..e1ebf0b4d67 --- /dev/null +++ b/src/plugins/fs_routes/mod_test.tsx @@ -0,0 +1,1002 @@ +import { App } from "../../app.ts"; +import { + type FreshFsItem, + fsRoutes, + type FsRoutesOptions, + sortRoutePaths, + type TESTING_ONLY__FsRoutesOptions, +} from "./mod.ts"; +import { delay, FakeServer } from "../../test_utils.ts"; +import { createFakeFs } from "../../test_utils.ts"; +import { expect } from "@std/expect"; +import type { HandlerByMethod, HandlerFn } from "../../handlers.ts"; +import type { Method } from "../../router.ts"; +import { parseHtml } from "../../../tests/test_utils.tsx"; + +async function createServer( + files: Record>, +): Promise { + const app = new App(); + + await fsRoutes( + app, + { + dir: ".", + loadIsland: async () => {}, + // deno-lint-ignore require-await + loadRoute: async (filePath) => { + const full = `routes/${filePath.replaceAll(/[\\]+/g, "/")}`; + if (full in files) { + return files[full]; + } + throw new Error(`Mock FS: file ${full} not found`); + }, + _fs: createFakeFs(files), + } as FsRoutesOptions & TESTING_ONLY__FsRoutesOptions, + ); + return new FakeServer(await app.handler()); +} + +Deno.test("fsRoutes - throws error when file has no exports", async () => { + const p = createServer({ "routes/index.tsx": {} }); + await expect(p).rejects.toMatch(/relevant exports/); +}); + +Deno.test("fsRoutes - registers HTTP methods on router", async () => { + const methodHandler: HandlerByMethod = { + GET: () => new Response("GET"), + POST: () => new Response("POST"), + PATCH: () => new Response("PATCH"), + PUT: () => new Response("PUT"), + DELETE: () => new Response("DELETE"), + HEAD: () => new Response("HEAD"), + }; + const server = await createServer({ + "routes/all.ts": { handlers: methodHandler }, + "routes/get.ts": { handlers: { GET: methodHandler.GET } }, + "routes/post.ts": { handlers: { POST: methodHandler.POST } }, + "routes/patch.ts": { handlers: { PATCH: methodHandler.PATCH } }, + "routes/put.ts": { handlers: { PUT: methodHandler.PUT } }, + "routes/delete.ts": { handlers: { DELETE: methodHandler.DELETE } }, + "routes/head.ts": { handlers: { HEAD: methodHandler.HEAD } }, + }); + + const methods: Method[] = ["GET", "POST", "PATCH", "PUT", "DELETE", "HEAD"]; + for (const method of methods) { + const name = method.toLowerCase() as Lowercase; + const res = await server[name]("/all"); + expect(res.status).toEqual(200); + expect(await res.text()).toEqual(method); + } + + // Check individual routes + for (const method of methods) { + const lower = method.toLowerCase() as Lowercase; + const res = await server[lower](`/${lower}`); + expect(res.status).toEqual(200); + expect(await res.text()).toEqual(method); + + // Check that all other methods are forbidden + for (const other of methods) { + if (other === method) continue; + + const name = other.toLowerCase() as Lowercase; + const res = await server[name](`/${lower}`); + await res.body?.cancel(); + expect(res.status).toEqual(405); + } + } +}); + +Deno.test("fsRoutes - registers fn handler for every method", async () => { + const handler: HandlerFn = () => new Response("ok"); + const server = await createServer({ + "routes/all.ts": { handlers: handler }, + }); + + const methods: Method[] = ["GET", "POST", "PATCH", "PUT", "DELETE", "HEAD"]; + for (const method of methods) { + const name = method.toLowerCase() as Lowercase; + const res = await server[name]("/all"); + expect(res.status).toEqual(200); + expect(await res.text()).toEqual("ok"); + } + + // Check individual routes + for (const method of methods) { + const lower = method.toLowerCase() as Lowercase; + const res = await server[lower]("/all"); + expect(res.status).toEqual(200); + expect(await res.text()).toEqual("ok"); + } +}); + +Deno.test("fsRoutes - renders component without handler", async () => { + const server = await createServer({ + "routes/all.ts": { default: () =>

foo

}, + }); + + const res = await server.get("/all"); + expect(res.status).toEqual(200); + expect(res.headers.get("Content-Type")).toEqual("text/html; charset=utf-8"); + + const doc = parseHtml(await res.text()); + // deno-lint-ignore no-explicit-any + expect((doc.body.firstChild as any).outerHTML).toEqual( + "

foo

", + ); +}); + +Deno.test("fsRoutes - sorts routes", async () => { + const server = await createServer({ + "routes/[id].ts": { handler: () => new Response("fail") }, + "routes/all.ts": { handler: () => new Response("ok") }, + }); + + const res = await server.get("/all"); + expect(await res.text()).toEqual("ok"); +}); + +Deno.test("fsRoutes - serve index", async () => { + const server = await createServer({ + "routes/[id].ts": { handler: () => new Response("fail") }, + "routes/index.ts": { handler: () => new Response("ok") }, + }); + + const res = await server.get("/"); + expect(await res.text()).toEqual("ok"); +}); + +Deno.test("fsRoutes - add middleware for function handler", async () => { + const server = await createServer<{ text: string }>({ + "routes/[id].ts": { handler: (ctx) => new Response(ctx.state.text) }, + "routes/index.ts": { handler: (ctx) => new Response(ctx.state.text) }, + "routes/none.ts": { default: (ctx) =>
{ctx.state.text}
}, + "routes/_middleware.ts": { + handler(ctx) { + ctx.state.text = "ok"; + return ctx.next(); + }, + }, + }); + + let res = await server.get("/"); + expect(await res.text()).toEqual("ok"); + + res = await server.get("/foo"); + expect(await res.text()).toEqual("ok"); + + res = await server.get("/none"); + const doc = parseHtml(await res.text()); + expect(doc.body.firstChild?.textContent).toEqual("ok"); +}); + +Deno.test("fsRoutes - nested middlewares", async () => { + const server = await createServer<{ text: string }>({ + "routes/_middleware.ts": { + handler: (ctx) => { + ctx.state.text = "A"; + return ctx.next(); + }, + }, + "routes/foo/_middleware.ts": { + handler: (ctx) => { + ctx.state.text += "B"; + return ctx.next(); + }, + }, + "routes/foo/index.ts": { default: (ctx) =>
{ctx.state.text}
}, + }); + + const res = await server.get("/foo"); + const doc = parseHtml(await res.text()); + expect(doc.body.firstChild?.textContent).toEqual("AB"); +}); + +Deno.test("fsRoutes - middleware array", async () => { + const server = await createServer<{ text: string }>({ + "routes/_middleware.ts": { + handler: [ + (ctx) => { + ctx.state.text = "A"; + return ctx.next(); + }, + (ctx) => { + ctx.state.text += "B"; + return ctx.next(); + }, + ], + }, + "routes/foo/_middleware.ts": { + handler: (ctx) => { + ctx.state.text += "C"; + return ctx.next(); + }, + }, + "routes/foo/index.ts": { default: (ctx) =>
{ctx.state.text}
}, + }); + + const res = await server.get("/foo"); + const doc = parseHtml(await res.text()); + expect(doc.body.firstChild?.textContent).toEqual("ABC"); +}); + +Deno.test("fsRoutes - combined", async () => { + const server = await createServer<{ text: string }>({ + "routes/foo/bar.ts": { + default: (ctx) =>
{ctx.state.text}
, + }, + "routes/foo/_middleware.ts": { + handler: (ctx) => { + ctx.state.text = "ok"; + return ctx.next(); + }, + }, + "routes/_middleware.ts": { + handler: (ctx) => { + ctx.state.text = "ok"; + return ctx.next(); + }, + }, + }); + + const res = await server.get("/foo/bar"); + const doc = parseHtml(await res.text()); + expect(doc.body.firstChild?.textContent).toEqual("ok"); +}); + +Deno.test("fsRoutes - prepend _app", async () => { + const server = await createServer({ + "routes/foo/bar.ts": { + default: () => <>foo_bar, + }, + "routes/foo.ts": { + default: () => <>foo, + }, + "routes/_app.tsx": { + default: (ctx) => ( +
+ app/ +
+ ), + }, + }); + + let res = await server.get("/foo/bar"); + let doc = parseHtml(await res.text()); + expect(doc.body.firstChild?.textContent).toEqual("app/foo_bar"); + + res = await server.get("/foo"); + doc = parseHtml(await res.text()); + expect(doc.body.firstChild?.textContent).toEqual("app/foo"); +}); + +Deno.test("fsRoutes - prepend _layout", async () => { + const server = await createServer({ + "routes/foo/bar.ts": { + default: () => <>foo_bar, + }, + "routes/foo.ts": { + default: () => <>foo, + }, + "routes/_layout.tsx": { + default: (ctx) => ( + <> + layout/ + + ), + }, + "routes/_app.tsx": { + default: (ctx) => ( +
+ app/ +
+ ), + }, + }); + + let res = await server.get("/foo/bar"); + let doc = parseHtml(await res.text()); + expect(doc.body.firstChild?.textContent).toEqual("app/layout/foo_bar"); + + res = await server.get("/foo"); + doc = parseHtml(await res.text()); + expect(doc.body.firstChild?.textContent).toEqual("app/layout/foo"); +}); + +Deno.test("fsRoutes - nested _layout", async () => { + const server = await createServer({ + "routes/foo/bar.ts": { + default: () => <>foo_bar, + }, + "routes/foo.ts": { + default: () => <>foo, + }, + "routes/foo/_layout.tsx": { + default: (ctx) => ( + <> + layout_foo_bar/ + + ), + }, + "routes/_layout.tsx": { + default: (ctx) => ( + <> + layout/ + + ), + }, + "routes/_app.tsx": { + default: (ctx) => ( +
+ app/ +
+ ), + }, + }); + + let res = await server.get("/foo/bar"); + let doc = parseHtml(await res.text()); + expect(doc.body.firstChild?.textContent).toEqual( + "app/layout/layout_foo_bar/foo_bar", + ); + + res = await server.get("/foo"); + doc = parseHtml(await res.text()); + expect(doc.body.firstChild?.textContent).toEqual("app/layout/foo"); +}); + +Deno.test("fsRoutes - _layout skip if not present", async () => { + const server = await createServer({ + "routes/foo/bar/baz.ts": { + default: () => <>foo_bar_baz, + }, + "routes/foo/_layout.tsx": { + default: (ctx) => ( +
+ layout_foo/ +
+ ), + }, + }); + + const res = await server.get("/foo/bar/baz"); + const doc = parseHtml(await res.text()); + expect(doc.body.firstChild?.textContent).toEqual("layout_foo/foo_bar_baz"); +}); + +Deno.test("fsRoutes - _layout file types", async () => { + const server = await createServer({ + "routes/js/index.js": { + default: () => <>js, + }, + "routes/js/_layout.js": { + default: (ctx) => ( +
+ layout_js/ +
+ ), + }, + "routes/jsx/index.jsx": { + default: () => <>jsx, + }, + "routes/jsx/_layout.jsx": { + default: (ctx) => ( +
+ layout_jsx/ +
+ ), + }, + "routes/ts/index.ts": { + default: () => <>ts, + }, + "routes/ts/_layout.tsx": { + default: (ctx) => ( +
+ layout_ts/ +
+ ), + }, + "routes/tsx/index.tsx": { + default: () => <>tsx, + }, + "routes/tsx/_layout.tsx": { + default: (ctx) => ( +
+ layout_tsx/ +
+ ), + }, + }); + + const res = await server.get("/js"); + const doc = parseHtml(await res.text()); + expect(doc.body.firstChild?.textContent).toEqual("layout_js/js"); +}); + +Deno.test("fsRoutes - _layout disable _app", async () => { + const server = await createServer({ + "routes/index.tsx": { + default: () => <>route, + }, + "routes/_layout.tsx": { + config: { + skipAppWrapper: true, + }, + default: (ctx) => ( + <> + layout/ + + ), + }, + "routes/_app.tsx": { + default: (ctx) => ( +
+ app/ +
+ ), + }, + }); + + const res = await server.get("/"); + const doc = parseHtml(await res.text()); + expect(doc.body.firstChild?.textContent).toEqual("layout/route"); +}); + +Deno.test( + "fsRoutes - _layout disable _app + inherited _layouts", + async () => { + const server = await createServer({ + "routes/sub/sub2/index.tsx": { + default: () => <>sub_sub2, + }, + "routes/sub/sub2/_layout.tsx": { + default: (ctx) => ( + <> + layout_sub_sub2/ + + ), + }, + "routes/sub/_layout.tsx": { + config: { + skipAppWrapper: true, + skipInheritedLayouts: true, + }, + default: (ctx) => ( + <> + layout_sub/ + + ), + }, + "routes/_layout.tsx": { + default: (ctx) => ( + <> + layout/ + + ), + }, + "routes/_app.tsx": { + default: (ctx) => ( +
+ app/ +
+ ), + }, + }); + + const res = await server.get("/sub/sub2"); + const doc = parseHtml(await res.text()); + expect(doc.body.firstChild?.textContent).toEqual( + "layout_sub/layout_sub_sub2/sub_sub2", + ); + }, +); + +Deno.test("fsRoutes - route overrides _layout", async () => { + const server = await createServer({ + "routes/index.tsx": { + config: { + skipInheritedLayouts: true, + }, + default: () => <>route, + }, + "routes/_layout.tsx": { + default: (ctx) => ( +
+ layout/ +
+ ), + }, + }); + + const res = await server.get("/"); + const doc = parseHtml(await res.text()); + expect(doc.body.firstChild?.textContent).toEqual("route"); +}); + +Deno.test("fsRoutes - route overrides _app", async () => { + const server = await createServer({ + "routes/index.tsx": { + config: { + skipAppWrapper: true, + }, + default: () => <>route, + }, + "routes/_app.tsx": { + default: (ctx) => ( +
+ app/ +
+ ), + }, + // Add some more routes on same level + "routes/a.tsx": { default: () => <>a }, + "routes/b.tsx": { default: () => <>b }, + "routes/c.tsx": { default: () => <>c }, + }); + + const res = await server.get("/"); + const doc = parseHtml(await res.text()); + expect(doc.body.firstChild?.textContent).toEqual("route"); +}); + +Deno.test("fsRoutes - handler return data", async () => { + const server = await createServer({ + "routes/index.tsx": { + handler: () => { + return { data: "foo", status: 404 }; + }, + default: (ctx) => { + // deno-lint-ignore no-explicit-any + return

{ctx.data as any}

; + }, + }, + }); + + const res = await server.get("/"); + const doc = parseHtml(await res.text()); + expect(doc.body.firstChild?.textContent).toEqual("foo"); +}); + +Deno.test("fsRoutes - _404", async () => { + const server = await createServer({ + "routes/_404.tsx": { + default: () => { + return
Custom 404 - Not Found
; + }, + }, + "routes/index.tsx": { + handlers: () => { + throw new Error("ok"); + }, + }, + }); + + const res = await server.get("/invalid"); + const content = await res.text(); + expect(content).toContain("Custom 404 - Not Found"); +}); + +Deno.test("fsRoutes - _500", async () => { + const server = await createServer({ + "routes/_500.tsx": { + default: () => { + return
Custom Error Page
; + }, + }, + "routes/index.tsx": { + handlers: () => { + throw new Error("ok"); + }, + }, + }); + + const res = await server.get("/"); + const content = await res.text(); + expect(content).toContain("Custom Error Page"); +}); + +Deno.test("fsRoutes - _error", async () => { + const server = await createServer({ + "routes/_error.tsx": { + default: () => { + return
Custom Error Page
; + }, + }, + "routes/index.tsx": { + handlers: () => { + throw new Error("ok"); + }, + }, + }); + + const res = await server.get("/"); + const content = await res.text(); + expect(content).toContain("Custom Error Page"); +}); + +Deno.test("fsRoutes - _error nested", async () => { + const server = await createServer({ + "routes/_error.tsx": { + handlers: () => { + throw new Error("fail"); + }, + }, + "routes/foo/_error.tsx": { + handlers: (ctx) => { + return new Response((ctx.error as Error).message); + }, + }, + "routes/foo/index.tsx": { + handlers: () => { + throw new Error("ok"); + }, + }, + }); + + const res = await server.get("/foo"); + expect(await res.text()).toEqual("ok"); +}); + +Deno.test("fsRoutes - _error nested throw", async () => { + const server = await createServer({ + "routes/_error.tsx": { + handlers: (ctx) => { + return new Response((ctx.error as Error).message); + }, + }, + "routes/foo/_error.tsx": { + handlers: () => { + throw new Error("ok"); + }, + }, + "routes/foo/index.tsx": { + handlers: () => { + throw new Error("ok"); + }, + }, + }); + + const res = await server.get("/foo"); + expect(await res.text()).toEqual("ok"); +}); + +Deno.test("fsRoutes - _error render component", async () => { + const server = await createServer({ + "routes/_error.tsx": { + default: (ctx) => { + return
{(ctx.error as Error).message}
; + }, + }, + "routes/foo/_error.tsx": { + handlers: () => { + throw new Error("ok"); + }, + }, + "routes/foo/index.tsx": { + handlers: () => { + throw new Error("ok"); + }, + }, + }); + + const res = await server.get("/foo"); + const doc = parseHtml(await res.text()); + expect(doc.body.firstChild?.textContent).toEqual("ok"); +}); + +Deno.test("fsRoutes - _error render on 404", async () => { + // deno-lint-ignore no-explicit-any + let error: any = null; + const server = await createServer({ + "routes/_error.tsx": { + default: (ctx) => { + // deno-lint-ignore no-explicit-any + error = ctx.error as any; + return

ok

; + }, + }, + "routes/foo/_error.tsx": { + default: (ctx) => { + // deno-lint-ignore no-explicit-any + error = ctx.error as any; + return

ok foo

; + }, + }, + "routes/foo/index.tsx": { + default: () => { + return

ignore

; + }, + }, + }); + + let res = await server.get("/foo/a"); + let doc = parseHtml(await res.text()); + expect(doc.body.firstChild?.textContent).toEqual("ok foo"); + expect(error?.status).toEqual(404); + + res = await server.get("/"); + doc = parseHtml(await res.text()); + expect(doc.body.firstChild?.textContent).toEqual("ok"); + expect(error?.status).toEqual(404); +}); + +Deno.test("fsRoutes - skip _error component in non-error", async () => { + const server = await createServer({ + "routes/_error.tsx": { + default: function errorComp() { + return
fail
; + }, + }, + "routes/index.tsx": { + default: () =>
ok
, + }, + }); + + const res = await server.get("/"); + const doc = parseHtml(await res.text()); + expect(doc.body.firstChild?.textContent).toEqual("ok"); +}); + +Deno.test("fsRoutes - route group resolve index", async () => { + const server = await createServer<{ text: string }>({ + "routes/(foo)/_layout.tsx": { + default: (ctx) => ( +
+ layout/ +
+ ), + }, + "routes/(foo)/index.tsx": { + default: () => <>ok, + }, + }); + + const res = await server.get("/"); + const doc = parseHtml(await res.text()); + expect(doc.body.firstChild?.textContent).toEqual("layout/ok"); +}); + +Deno.test("fsRoutes - route group ignores (_...) folders", async () => { + const server = await createServer<{ text: string }>({ + "routes/(_foo)/index.tsx": { + default: () =>
fail
, + }, + "routes/(foo)/index.tsx": { + default: () =>
ok
, + }, + }); + + const res = await server.get("/"); + const doc = parseHtml(await res.text()); + expect(doc.body.firstChild?.textContent).toEqual("ok"); +}); + +Deno.test("fsRoutes - route group specific templates", async () => { + const server = await createServer<{ text: string }>({ + "routes/(foo)/_error.tsx": { + default: () =>
fail foo
, + }, + "routes/(foo)/_layout.tsx": { + default: (ctx) => ( +
+ {ctx.state.text}/(foo)_layout/ +
+ ), + }, + "routes/(foo)/_middleware.tsx": { + handlers: (ctx) => { + ctx.state.text = "(foo)_middleware"; + return ctx.next(); + }, + }, + "routes/(foo)/foo.tsx": { + default: () =>
foo
, + }, + "routes/(foo)/foo_error.tsx": { + default: () => { + throw new Error("fail"); + }, + }, + "routes/(bar)/_error.tsx": { + default: () =>
fail bar
, + }, + "routes/(bar)/_layout.tsx": { + default: (ctx) => ( +
+ {ctx.state.text}/(bar)_layout/ +
+ ), + }, + "routes/(bar)/_middleware.tsx": { + handlers: (ctx) => { + ctx.state.text = "(bar)_middleware"; + return ctx.next(); + }, + }, + "routes/(bar)/bar.tsx": { + default: () =>
bar
, + }, + "routes/(bar)/bar_error.tsx": { + default: () => { + throw new Error("fail"); + }, + }, + }); + + let res = await server.get("/foo"); + let doc = parseHtml(await res.text()); + expect(doc.body.firstChild?.textContent).toEqual( + "(foo)_middleware/(foo)_layout/foo", + ); + res = await server.get("/foo_error"); + doc = parseHtml(await res.text()); + expect(doc.body.firstChild?.textContent).toEqual("fail foo"); + + res = await server.get("/bar"); + doc = parseHtml(await res.text()); + expect(doc.body.firstChild?.textContent).toEqual( + "(bar)_middleware/(bar)_layout/bar", + ); + + res = await server.get("/bar_error"); + doc = parseHtml(await res.text()); + expect(doc.body.firstChild?.textContent).toEqual("fail bar"); +}); + +Deno.test("fsRoutes - async route components", async () => { + const server = await createServer<{ text: string }>({ + "routes/_error.tsx": { + default: async () => { + await delay(1); + return
fail foo
; + }, + }, + "routes/_layout.tsx": { + default: async (ctx) => { + await delay(1); + return ( +
+ {ctx.state.text}/_layout/ +
+ ); + }, + }, + "routes/foo.tsx": { + default: async () => { + await delay(1); + return
foo
; + }, + }, + "routes/foo_error.tsx": { + default: async () => { + await delay(1); + throw new Error("fail"); + }, + }, + }); + + let res = await server.get("/foo"); + let doc = parseHtml(await res.text()); + expect(doc.body.firstChild?.textContent).toEqual("/_layout/foo"); + + res = await server.get("/foo_error"); + doc = parseHtml(await res.text()); + expect(doc.body.firstChild?.textContent).toEqual("fail foo"); +}); + +Deno.test("fsRoutes - async route components returning response", async () => { + const server = await createServer<{ text: string }>({ + "routes/_app.tsx": { + default: async (ctx) => { + await delay(1); + if (ctx.url.searchParams.has("app")) { + return new Response("_app"); + } + return ( +
+ _app/ +
+ ); + }, + }, + "routes/_layout.tsx": { + default: async (ctx) => { + await delay(1); + if (ctx.url.searchParams.has("layout")) { + return new Response("_layout"); + } + return ( +
+ _layout/ +
+ ); + }, + }, + "routes/index.tsx": { + default: async (ctx) => { + await delay(1); + if (ctx.url.searchParams.has("index")) { + return new Response("index"); + } + return
index
; + }, + }, + }); + + let res = await server.get("/"); + const doc = parseHtml(await res.text()); + expect(doc.body.firstChild?.textContent).toEqual("_app/_layout/index"); + + res = await server.get("/?app"); + let text = await res.text(); + expect(text).toEqual("_app"); + + res = await server.get("/?layout"); + text = await res.text(); + expect(text).toEqual("_layout"); + + res = await server.get("/?index"); + text = await res.text(); + expect(text).toEqual("index"); +}); + +Deno.test("fsRoutes - sortRoutePaths", () => { + let routes = [ + "/foo/[id]", + "/foo/[...slug]", + "/foo/bar", + "/foo/_layout", + "/foo/index", + "/foo/_middleware", + "/foo/bar/_middleware", + "/foo/_error", + "/foo/bar/index", + "/foo/bar/_error", + "/_error", + "/foo/bar/[...foo]", + "/foo/bar/baz", + "/foo/bar/_layout", + ]; + let sorted = [ + "/_error", + "/foo/_error", + "/foo/_middleware", + "/foo/_layout", + "/foo/index", + "/foo/bar/_error", + "/foo/bar/_middleware", + "/foo/bar/_layout", + "/foo/bar/index", + "/foo/bar/baz", + "/foo/bar/[...foo]", + "/foo/bar", + "/foo/[id]", + "/foo/[...slug]", + ]; + routes.sort(sortRoutePaths); + expect(routes).toEqual(sorted); + + routes = [ + "/js/index.js", + "/js/_layout.js", + "/jsx/index.jsx", + "/jsx/_layout.jsx", + "/ts/index.ts", + "/ts/_layout.tsx", + "/tsx/index.tsx", + "/tsx/_layout.tsx", + ]; + routes.sort(sortRoutePaths); + sorted = [ + "/js/_layout.js", + "/js/index.js", + "/jsx/_layout.jsx", + "/jsx/index.jsx", + "/ts/_layout.tsx", + "/ts/index.ts", + "/tsx/_layout.tsx", + "/tsx/index.tsx", + ]; + expect(routes).toEqual(sorted); +}); diff --git a/src/plugins/fs_routes/render_middleware.ts b/src/plugins/fs_routes/render_middleware.ts new file mode 100644 index 00000000000..7f1911338d3 --- /dev/null +++ b/src/plugins/fs_routes/render_middleware.ts @@ -0,0 +1,74 @@ +import { type AnyComponent, h, type RenderableProps, type VNode } from "preact"; +import type { MiddlewareFn } from "../../middlewares/mod.ts"; +import type { HandlerFn, Render } from "../../handlers.ts"; +import type { PageProps } from "../../runtime/server/mod.tsx"; + +export type AsyncAnyComponent

= { + ( + props: RenderableProps

, + // deno-lint-ignore no-explicit-any + context?: any, + // deno-lint-ignore no-explicit-any + ): Promise | Response | null>; + displayName?: string; + defaultProps?: Partial

| undefined; +}; + +export function renderMiddleware( + components: Array< + | AnyComponent> + | AsyncAnyComponent> + >, + handler: HandlerFn | undefined, +): MiddlewareFn { + return async (ctx) => { + let result: Render | undefined; + if (handler !== undefined) { + const res = await handler(ctx); + + if (res instanceof Response) { + return res; + } + + // deno-lint-ignore no-explicit-any + result = res as any; + } + + if (components.length === 0) { + throw new Error(`Did not receive any components to render.`); + } + + let vnode: VNode | null = null; + for (let i = components.length - 1; i >= 0; i--) { + const child = vnode; + const Component = () => child; + + const fn = components[i]; + + if ( + typeof fn === "function" && + fn.constructor.name === "AsyncFunction" + ) { + const result = (await fn({ ...ctx, Component })) as VNode | Response; + if (result instanceof Response) { + return result; + } + vnode = result; + } else { + // deno-lint-ignore no-explicit-any + vnode = h(components[i] as any, { + config: ctx.config, + url: ctx.url, + req: ctx.req, + params: ctx.params, + state: ctx.state, + Component, + error: ctx.error, + data: result?.data ?? {}, + }) as VNode; + } + } + + return ctx.render(vnode!); + }; +} diff --git a/src/plugins/fs_routes/render_middleware_test.tsx b/src/plugins/fs_routes/render_middleware_test.tsx new file mode 100644 index 00000000000..c04bcd019ee --- /dev/null +++ b/src/plugins/fs_routes/render_middleware_test.tsx @@ -0,0 +1,125 @@ +import { expect } from "@std/expect"; +import { delay, serveMiddleware } from "../../test_utils.ts"; +import { renderMiddleware } from "./render_middleware.ts"; +import { parseHtml } from "../../../tests/test_utils.tsx"; + +Deno.test("renderMiddleware - responds with HTML", async () => { + const server = await serveMiddleware( + renderMiddleware([() =>

ok

], undefined), + ); + + const res = await server.get("/"); + expect(res.headers.get("Content-Type")).toEqual("text/html; charset=utf-8"); + + const doc = parseHtml(await res.text()); + // deno-lint-ignore no-explicit-any + expect((doc.body.firstChild as any).outerHTML).toEqual("

ok

"); +}); + +Deno.test("renderMiddleware - bypass rendering when handler returns Response", async () => { + const server = await serveMiddleware( + renderMiddleware( + [() =>

fail

], + () => new Response(null, { status: 204 }), + ), + ); + + const res = await server.get("/"); + expect(res.status).toEqual(204); + expect(res.body).toEqual(null); +}); + +Deno.test("renderMiddleware - chain components", async () => { + const server = await serveMiddleware( + renderMiddleware( + [ + (ctx) => ( +
+ c1 +
+ ), + (ctx) => ( + <> + c2 + + ), + () => <>c3, + ], + undefined, + ), + ); + + const res = await server.get("/"); + expect(res.status).toEqual(200); + + const doc = parseHtml(await res.text()); + expect(doc.body.firstChild?.textContent).toEqual("c1c2c3"); +}); + +Deno.test("renderMiddleware - chain async components", async () => { + const server = await serveMiddleware( + renderMiddleware( + [ + async (ctx) => { + await delay(1); + return ( +
+ c1 +
+ ); + }, + async (ctx) => { + await delay(1); + return ( + <> + c2 + + ); + }, + async () => { + await delay(1); + return <>c3; + }, + ], + undefined, + ), + ); + + const res = await server.get("/"); + expect(res.status).toEqual(200); + + const doc = parseHtml(await res.text()); + expect(doc.body.firstChild!.textContent).toEqual("c1c2c3"); +}); + +Deno.test("renderMiddleware - async components Response bail out", async () => { + const server = await serveMiddleware( + renderMiddleware( + [ + async (ctx) => { + await delay(1); + return ( +
+ c1 +
+ ); + }, + async (ctx) => { + await delay(1); + + return new Response("foo"); + }, + async () => { + await delay(1); + return <>c3; + }, + ], + undefined, + ), + ); + + const res = await server.get("/"); + expect(res.status).toEqual(200); + + expect(await res.text()).toEqual("foo"); +}); diff --git a/src/router.ts b/src/router.ts new file mode 100644 index 00000000000..4d1c7772f27 --- /dev/null +++ b/src/router.ts @@ -0,0 +1,231 @@ +export type Method = "HEAD" | "GET" | "POST" | "PATCH" | "PUT" | "DELETE"; + +export interface Route { + path: string | URLPattern; + method: Method | "ALL"; + handlers: T[]; +} + +export interface RouteResult { + params: Record; + handlers: T[][]; + methodMatch: boolean; + patternMatch: boolean; +} + +export interface Router { + _routes: Route[]; + _middlewares: T[]; + addMiddleware(fn: T): void; + add( + method: Method | "ALL", + pathname: string | URLPattern, + handlers: T[], + ): void; + match(method: Method, url: URL): RouteResult; +} + +export const IS_PATTERN = /[*:{}+?()]/; + +export function mergePaths(a: string, b: string) { + if (a === "" || a === "/" || a === "*") return b; + if (b === "/") return a; + if (a.endsWith("/")) { + return a.slice(0, -1) + b; + } else if (!b.startsWith("/")) { + return a + "/" + b; + } + return a + b; +} + +export class UrlPatternRouter implements Router { + readonly _routes: Route[] = []; + readonly _middlewares: T[] = []; + + addMiddleware(fn: T): void { + this._middlewares.push(fn); + } + + add(method: Method | "ALL", pathname: string | URLPattern, handlers: T[]) { + if ( + typeof pathname === "string" && pathname !== "*" && + IS_PATTERN.test(pathname) + ) { + this._routes.push({ + path: new URLPattern({ pathname }), + handlers, + method, + }); + } else { + this._routes.push({ + path: pathname, + handlers, + method, + }); + } + } + + match(method: Method, url: URL): RouteResult { + const result: RouteResult = { + params: {}, + handlers: [], + methodMatch: false, + patternMatch: false, + }; + + if (this._middlewares.length > 0) { + result.handlers.push(this._middlewares); + } + + for (let i = 0; i < this._routes.length; i++) { + const route = this._routes[i]; + + // Fast path for string based routes which are expected + // to be either wildcard `*` match or an exact pathname match. + if ( + typeof route.path === "string" && + (route.path === "*" || route.path === url.pathname) + ) { + if (route.method !== "ALL") { + result.patternMatch = true; + } + + if (route.method === "ALL" || route.method === method) { + result.handlers.push(route.handlers); + + if (route.path === "*" && route.method === "ALL") { + continue; + } + + result.methodMatch = true; + + return result; + } + } else if (route.path instanceof URLPattern) { + const match = route.path.exec(url); + if (match !== null) { + if (route.method !== "ALL") { + result.patternMatch = true; + } + + if (route.method === "ALL" || route.method === method) { + result.handlers.push(route.handlers); + + // Decode matched params + for (const [key, value] of Object.entries(match.pathname.groups)) { + if (value !== undefined) { + result.params[key] = decodeURI(value); + } + } + + if (route.method === "ALL") { + continue; + } + + result.methodMatch = true; + return result; + } + } + } + } + + return result; + } +} + +/** + * Transform a filesystem URL path to a `path-to-regex` style matcher. + */ +export function pathToPattern(path: string): string { + const parts = path.split("/"); + if (parts[parts.length - 1] === "index") { + if (parts.length === 1) { + return "/"; + } + parts.pop(); + } + + let route = ""; + + for (let i = 0; i < parts.length; i++) { + const part = parts[i]; + + // Case: /[...foo].tsx + if (part.startsWith("[...") && part.endsWith("]")) { + route += `/:${part.slice(4, part.length - 1)}*`; + continue; + } + + // Route groups like /foo/(bar) should not be included in URL + // matching. They are transparent and need to be removed here. + // Case: /foo/(bar) -> /foo + // Case: /foo/(bar)/bob -> /foo/bob + // Case: /(foo)/bar -> /bar + if (part.startsWith("(") && part.endsWith(")")) { + continue; + } + + // Disallow neighbouring params like `/[id][bar].tsx` because + // it's ambiguous where the `id` param ends and `bar` begins. + if (part.includes("][")) { + throw new SyntaxError( + `Invalid route pattern: "${path}". A parameter cannot be followed by another parameter without any characters in between.`, + ); + } + + // Case: /[[id]].tsx + // Case: /[id].tsx + // Case: /[id]@[bar].tsx + // Case: /[id]-asdf.tsx + // Case: /[id]-asdf[bar].tsx + // Case: /asdf[bar].tsx + let pattern = ""; + let groupOpen = 0; + let optional = false; + for (let j = 0; j < part.length; j++) { + const char = part[j]; + if (char === "[") { + if (part[j + 1] === "[") { + // Disallow optional dynamic params like `foo-[[bar]]` + if (part[j - 1] !== "/" && !!part[j - 1]) { + throw new SyntaxError( + `Invalid route pattern: "${path}". An optional parameter needs to be a full segment.`, + ); + } + groupOpen++; + optional = true; + pattern += "{/"; + j++; + } + pattern += ":"; + groupOpen++; + } else if (char === "]") { + if (part[j + 1] === "]") { + // Disallow optional dynamic params like `[[foo]]-bar` + if (part[j + 2] !== "/" && !!part[j + 2]) { + throw new SyntaxError( + `Invalid route pattern: "${path}". An optional parameter needs to be a full segment.`, + ); + } + groupOpen--; + pattern += "}?"; + j++; + } + if (--groupOpen < 0) { + throw new SyntaxError(`Invalid route pattern: "${path}"`); + } + } else { + pattern += char; + } + } + + route += (optional ? "" : "/") + pattern; + } + + // Case: /(group)/index.tsx + if (route === "") { + route = "/"; + } + + return route; +} diff --git a/src/router_test.ts b/src/router_test.ts new file mode 100644 index 00000000000..7c2e46ef52d --- /dev/null +++ b/src/router_test.ts @@ -0,0 +1,180 @@ +import { expect } from "@std/expect"; +import { + IS_PATTERN, + mergePaths, + pathToPattern, + UrlPatternRouter, +} from "./router.ts"; + +Deno.test("IS_PATTERN", () => { + expect(IS_PATTERN.test("/foo")).toEqual(false); + expect(IS_PATTERN.test("/foo/bar/baz.jpg")).toEqual(false); + expect(IS_PATTERN.test("/foo/:path")).toEqual(true); + expect(IS_PATTERN.test("/foo/*")).toEqual(true); + expect(IS_PATTERN.test("/foo{/bar}?")).toEqual(true); + expect(IS_PATTERN.test("/foo/(\\d+)")).toEqual(true); + expect(IS_PATTERN.test("/foo/(a)")).toEqual(true); +}); + +Deno.test("UrlPatternRouter - GET get first match", () => { + const router = new UrlPatternRouter(); + const A = () => {}; + const B = () => {}; + const C = () => {}; + router.add("GET", "/", [A]); + router.add("GET", "/", [B]); + router.add("GET", "/", [C]); + + const res = router.match("GET", new URL("/", "http://localhost")); + expect(res).toEqual({ + params: {}, + handlers: [[A]], + methodMatch: true, + patternMatch: true, + }); +}); + +Deno.test("UrlPatternRouter - GET get matches with middlewares", () => { + const router = new UrlPatternRouter(); + const A = () => {}; + const B = () => {}; + const C = () => {}; + router.add("ALL", "*", [A]); + router.add("ALL", "*", [B]); + router.add("GET", "/", [C]); + + const res = router.match("GET", new URL("/", "http://localhost")); + expect(res).toEqual({ + params: {}, + handlers: [[A], [B], [C]], + methodMatch: true, + patternMatch: true, + }); +}); + +Deno.test("UrlPatternRouter - GET extract params", () => { + const router = new UrlPatternRouter(); + const A = () => {}; + router.add("GET", new URLPattern({ pathname: "/:foo/:bar/c" }), [A]); + + let res = router.match("GET", new URL("/a/b/c", "http://localhost")); + expect(res).toEqual({ + params: { foo: "a", bar: "b" }, + handlers: [[A]], + methodMatch: true, + patternMatch: true, + }); + + // Decode params + res = router.match("GET", new URL("/a%20a/b/c", "http://localhost")); + expect(res).toEqual({ + params: { foo: "a a", bar: "b" }, + handlers: [[A]], + methodMatch: true, + patternMatch: true, + }); +}); + +Deno.test("UrlPatternRouter - Wrong method match", () => { + const router = new UrlPatternRouter(); + const A = () => {}; + router.add("GET", "/foo", [A]); + + const res = router.match("POST", new URL("/foo", "http://localhost")); + expect(res).toEqual({ + params: {}, + handlers: [], + methodMatch: false, + patternMatch: true, + }); +}); + +Deno.test("UrlPatternRouter - wrong + correct method", () => { + const router = new UrlPatternRouter(); + const A = () => {}; + const B = () => {}; + router.add("GET", "/foo", [A]); + router.add("POST", "/foo", [B]); + + const res = router.match("POST", new URL("/foo", "http://localhost")); + expect(res).toEqual({ + params: {}, + handlers: [[B]], + methodMatch: true, + patternMatch: true, + }); +}); + +Deno.test("UrlPatternRouter - convert patterns automatically", () => { + const router = new UrlPatternRouter(); + const A = () => {}; + router.add("GET", "/books/:id", [A]); + + const res = router.match("GET", new URL("/books/foo", "http://localhost")); + expect(res).toEqual({ + params: { + id: "foo", + }, + handlers: [[A]], + methodMatch: true, + patternMatch: true, + }); +}); + +Deno.test("pathToPattern", async (t) => { + await t.step("creates pattern", () => { + expect(pathToPattern("foo/bar")).toEqual("/foo/bar"); + }); + + await t.step("parses index routes", () => { + expect(pathToPattern("foo/index")).toEqual("/foo"); + }); + + await t.step("parses parameters", () => { + expect(pathToPattern("foo/[name]")).toEqual("/foo/:name"); + expect(pathToPattern("foo/[name]/bar/[bob]")).toEqual( + "/foo/:name/bar/:bob", + ); + }); + + await t.step("parses catchall", () => { + expect(pathToPattern("foo/[...name]")).toEqual("/foo/:name*"); + }); + + await t.step("parses multiple params in same part", () => { + expect(pathToPattern("foo/[mod]@[version]")).toEqual("/foo/:mod@:version"); + expect(pathToPattern("foo/[bar].json")).toEqual("/foo/:bar.json"); + expect(pathToPattern("foo/foo[bar]")).toEqual("/foo/foo:bar"); + }); + + await t.step("parses optional params", () => { + expect(pathToPattern("foo/[[name]]")).toEqual("/foo{/:name}?"); + expect(pathToPattern("foo/[name]/[[bob]]")).toEqual("/foo/:name{/:bob}?"); + expect(pathToPattern("foo/[[name]]/bar")).toEqual("/foo{/:name}?/bar"); + expect( + pathToPattern("foo/[[name]]/bar/[[bob]]"), + ).toEqual( + "/foo{/:name}?/bar{/:bob}?", + ); + }); + + await t.step("throws on invalid patterns", () => { + expect(() => pathToPattern("foo/[foo][bar]")).toThrow(); + expect(() => pathToPattern("foo/foo]")).toThrow(); + expect(() => pathToPattern("foo/[foo]]")).toThrow(); + expect(() => pathToPattern("foo/foo-[[name]]-bar/baz")).toThrow(); + expect(() => pathToPattern("foo/[[name]]-bar/baz")).toThrow(); + expect(() => pathToPattern("foo/foo-[[name]]/baz")).toThrow(); + expect(() => pathToPattern("foo/foo-[[name]]")).toThrow(); + expect(() => pathToPattern("foo/[[name]]-bar")).toThrow(); + }); +}); + +Deno.test("mergePaths", () => { + expect(mergePaths("", "")).toEqual(""); + expect(mergePaths("/", "/foo")).toEqual("/foo"); + expect(mergePaths("*", "/foo")).toEqual("/foo"); + expect(mergePaths("/foo/bar", "/baz")).toEqual("/foo/bar/baz"); + expect(mergePaths("/foo/bar/", "/baz")).toEqual("/foo/bar/baz"); + expect(mergePaths("/foo/bar", "baz")).toEqual("/foo/bar/baz"); +}); diff --git a/src/runtime/Partial.tsx b/src/runtime/Partial.tsx deleted file mode 100644 index caa837e5d10..00000000000 --- a/src/runtime/Partial.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { ComponentChildren, VNode } from "preact"; - -export interface PartialProps { - children?: ComponentChildren; - /** - * The name of the partial. This value must be unique across partials. - */ - name: string; - /** - * Define how the new HTML should be applied. - * @default {"replace"} - */ - mode?: "replace" | "prepend" | "append"; -} - -export function Partial(props: PartialProps): VNode { - // deno-lint-ignore no-explicit-any - return props.children as any; -} -Partial.displayName = "Partial"; diff --git a/src/runtime/active_url.ts b/src/runtime/active_url.ts deleted file mode 100644 index 5534eed906e..00000000000 --- a/src/runtime/active_url.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { VNode } from "preact"; -import { DATA_ANCESTOR, DATA_CURRENT } from "../constants.ts"; - -export const enum UrlMatchKind { - None, - Ancestor, - Current, -} - -export function matchesUrl(current: string, needle: string): UrlMatchKind { - let href = new URL(needle, "http://localhost").pathname; - if (href !== "/" && href.endsWith("/")) { - href = href.slice(0, -1); - } - - if (current !== "/" && current.endsWith("/")) { - current = current.slice(0, -1); - } - - if (current === href) { - return UrlMatchKind.Current; - } else if (current.startsWith(href + "/") || href === "/") { - return UrlMatchKind.Ancestor; - } - - return UrlMatchKind.None; -} - -/** - * Mark active or ancestor link - * Note: This function is used both on the server and the client - */ -export function setActiveUrl(vnode: VNode, pathname: string): void { - const props = vnode.props as Record; - const hrefProp = props.href; - if (typeof hrefProp === "string" && hrefProp.startsWith("/")) { - const match = matchesUrl(pathname, hrefProp); - if (match === UrlMatchKind.Current) { - props[DATA_CURRENT] = "true"; - props["aria-current"] = "page"; - } else if (match === UrlMatchKind.Ancestor) { - props[DATA_ANCESTOR] = "true"; - props["aria-current"] = "true"; - } - } -} diff --git a/src/runtime/build_id.ts b/src/runtime/build_id.ts index a7ad66a0ad9..1e9ddcd847d 100644 --- a/src/runtime/build_id.ts +++ b/src/runtime/build_id.ts @@ -1,2 +1,17 @@ -// Note: in the client build this file is replaced with a file exporting a static string -export { BUILD_ID } from "../server/build_id.ts"; +import { encodeHex } from "@std/encoding/hex"; + +export const DENO_DEPLOYMENT_ID = Deno.env.get("DENO_DEPLOYMENT_ID"); +const deploymentId = DENO_DEPLOYMENT_ID || + // For CI + Deno.env.get("GITHUB_SHA") || + crypto.randomUUID(); +const buildIdHash = await crypto.subtle.digest( + "SHA-1", + new TextEncoder().encode(deploymentId), +); + +export let BUILD_ID = encodeHex(buildIdHash); + +export function setBuildId(buildId: string) { + BUILD_ID = buildId; +} diff --git a/src/runtime/entrypoints/client.ts b/src/runtime/client/dev.ts similarity index 78% rename from src/runtime/entrypoints/client.ts rename to src/runtime/client/dev.ts index 909eeeab787..99f51a7c0d9 100644 --- a/src/runtime/entrypoints/client.ts +++ b/src/runtime/client/dev.ts @@ -1,3 +1,7 @@ +import "preact/debug"; +export * from "./mod.tsx"; +import { IS_BROWSER } from "../shared.ts"; + let ws: WebSocket; let revision = 0; @@ -67,8 +71,6 @@ function connect() { ws.addEventListener("error", handleError); } -connect(); - function disconnect() { ws.removeEventListener("open", onOpenWs); ws.removeEventListener("close", onCloseWs); @@ -108,21 +110,25 @@ function handleError(e: Event) { } } -addEventListener("message", (ev) => { - if (ev.origin !== location.origin) return; - if (typeof ev.data !== "string" || ev.data !== "close-error-overlay") { - return; - } +if (IS_BROWSER) { + connect(); - document.querySelector("#fresh-error-overlay")?.remove(); -}); + addEventListener("message", (ev) => { + if (ev.origin !== location.origin) return; + if (typeof ev.data !== "string" || ev.data !== "close-error-overlay") { + return; + } -// Disconnect when the tab becomes inactive and re-connect when it -// becomes active again -addEventListener("visibilitychange", () => { - if (document.hidden) { - disconnect(); - } else { - connect(); - } -}); + document.querySelector("#fresh-error-overlay")?.remove(); + }); + + // Disconnect when the tab becomes inactive and re-connect when it + // becomes active again + addEventListener("visibilitychange", () => { + if (document.hidden) { + disconnect(); + } else { + connect(); + } + }); +} diff --git a/src/runtime/client/mod.tsx b/src/runtime/client/mod.tsx new file mode 100644 index 00000000000..292cb13a647 --- /dev/null +++ b/src/runtime/client/mod.tsx @@ -0,0 +1,4 @@ +import "./polyfills.ts"; +import "./partials.ts"; +export { asset, IS_BROWSER, Partial, type PartialProps } from "../shared.ts"; +export { boot, revive } from "./reviver.ts"; diff --git a/src/runtime/client/partials.ts b/src/runtime/client/partials.ts new file mode 100644 index 00000000000..ae3034b4193 --- /dev/null +++ b/src/runtime/client/partials.ts @@ -0,0 +1,507 @@ +import { type ComponentChildren, h } from "preact"; +import { + CLIENT_NAV_ATTR, + DATA_ANCESTOR, + DATA_CURRENT, + matchesUrl, + PartialMode, + UrlMatchKind, +} from "../shared_internal.tsx"; +import { + ACTIVE_PARTIALS, + copyOldChildren, + CUSTOM_PARSER, + type DeserializedProps, + domToVNode, + ISLAND_REGISTRY, + Marker, + maybeHideMarker, + PartialComp, +} from "./reviver.ts"; +import { createRootFragment, isCommentNode, isElementNode } from "./reviver.ts"; +import type { PartialStateJson } from "../server/preact_hooks.tsx"; +import { parse } from "../../jsonify/parse.ts"; +import { INTERNAL_PREFIX } from "../../constants.ts"; + +export const PARTIAL_ATTR = "f-partial"; +export const PARTIAL_SEARCH_PARAM = "fresh-partial"; + +class NoPartialsError extends Error {} + +export interface FreshHistoryState { + index: number; + scrollX: number; + scrollY: number; +} + +function checkClientNavEnabled(el: HTMLElement) { + const setting = el.closest(`[${CLIENT_NAV_ATTR}]`); + if (setting === null) return false; + return setting.getAttribute(CLIENT_NAV_ATTR) !== "false"; +} + +// Keep track of history state to apply forward or backward animations +let index = history.state?.index || 0; +if (!history.state) { + const state: FreshHistoryState = { + index, + scrollX, + scrollY, + }; + history.replaceState(state, document.title); +} + +function maybeUpdateHistory(nextUrl: URL) { + // Only add history entry when URL is new. Still apply + // the partials because sometimes users click a link to + // "refresh" the current page. + if (nextUrl.href !== globalThis.location.href) { + const state: FreshHistoryState = { + index, + scrollX: globalThis.scrollX, + scrollY: globalThis.scrollY, + }; + + // Store current scroll position + history.replaceState({ ...state }, "", location.href); + + // Now store the new position + index++; + state.scrollX = 0; + state.scrollY = 0; + history.pushState(state, "", nextUrl.href); + } +} + +document.addEventListener("click", async (e) => { + let el = e.target; + if (el && el instanceof HTMLElement) { + const originalEl = el; + + // Check if we clicked inside an anchor link + if (el.nodeName !== "A") { + el = el.closest("a"); + } + + if ( + // Check that we're still dealing with an anchor tag + el && el instanceof HTMLAnchorElement && + // Check if it's an internal link + el.href && (!el.target || el.target === "_self") && + el.origin === location.origin && + // Check if it was a left click and not a right click + e.button === 0 && + // Check that the user doesn't press a key combo to open the + // link in a new tab or something + !(e.ctrlKey || e.metaKey || e.altKey || e.shiftKey || e.button) && + // Check that the event isn't aborted already + !e.defaultPrevented + ) { + const partial = el.getAttribute(PARTIAL_ATTR); + + // Check if the user opted out of client side navigation or if + // we're doing a fragment navigation. + if ( + el.getAttribute("href")?.startsWith("#") || + !checkClientNavEnabled(el) + ) { + return; + } + + // deno-lint-ignore no-explicit-any + const indicator = (el as any)._freshIndicator; + if (indicator !== undefined) { + indicator.value = true; + } + + e.preventDefault(); + + const nextUrl = new URL(el.href); + try { + maybeUpdateHistory(nextUrl); + + const partialUrl = new URL( + partial ? partial : nextUrl.href, + location.href, + ); + await fetchPartials(partialUrl); + updateLinks(nextUrl); + scrollTo({ left: 0, top: 0, behavior: "instant" }); + } finally { + if (indicator !== undefined) { + indicator.value = false; + } + } + } else { + let button: HTMLButtonElement | HTMLElement | null = originalEl; + // Check if we clicked on a button + if (button.nodeName !== "A") { + button = button.closest("button"); + } + + if ( + button !== null && button instanceof HTMLButtonElement && + (button.type !== "submit" || button.form === null) + ) { + const partial = button.getAttribute(PARTIAL_ATTR); + + // Check if the user opted out of client side navigation. + if ( + partial === null || + !checkClientNavEnabled(button) + ) { + return; + } + + const partialUrl = new URL( + partial, + location.href, + ); + await fetchPartials(partialUrl); + } + } + } +}); + +addEventListener("popstate", async (e) => { + // When state is `null` then the browser navigated to a document + // fragment. In this case we do nothing. + if (e.state === null) { + // Reset to browser default + if (history.scrollRestoration) { + history.scrollRestoration = "auto"; + } + return; + } + + const state: FreshHistoryState = history.state; + const nextIdx = state.index ?? index + 1; + index = nextIdx; + + const setting = document.querySelector(`[${CLIENT_NAV_ATTR}]`); + if (setting === null || setting.getAttribute(CLIENT_NAV_ATTR) === "false") { + location.reload(); + return; + } + + // We need to keep track of that ourselves since we do client side + // navigation. + if (history.scrollRestoration) { + history.scrollRestoration = "manual"; + } + + const url = new URL(location.href, location.origin); + try { + await fetchPartials(url); + updateLinks(url); + scrollTo({ + left: state.scrollX ?? 0, + top: state.scrollY ?? 0, + behavior: "instant", + }); + } catch (err) { + // If the response didn't contain a partial, then we can only + // do a reload. + if (err instanceof NoPartialsError) { + location.reload(); + return; + } + + throw err; + } +}); + +// Form submit +document.addEventListener("submit", async (e) => { + const el = e.target; + if (el !== null && el instanceof HTMLFormElement && !e.defaultPrevented) { + if ( + // Check if form has client nav enabled + !checkClientNavEnabled(el) || + // Bail out if submitter is set and client nav is disabled + (e.submitter !== null && !checkClientNavEnabled(e.submitter)) + ) { + return; + } + + const lowerMethod = + e.submitter?.getAttribute("formmethod")?.toLowerCase() ?? + el.method.toLowerCase(); + if ( + lowerMethod !== "get" && lowerMethod !== "post" && + lowerMethod !== "dialog" + ) { + return; + } + + const action = e.submitter?.getAttribute(PARTIAL_ATTR) ?? + e.submitter?.getAttribute("formaction") ?? + el.getAttribute(PARTIAL_ATTR) ?? el.action; + + if (action !== "") { + e.preventDefault(); + + const url = new URL(action, location.href); + + let init: RequestInit | undefined; + + // GET method appends form data via url search params + if (lowerMethod === "get") { + // TODO: Looks like constructor type for URLSearchParam is wrong + // deno-lint-ignore no-explicit-any + const qs = new URLSearchParams(new FormData(el) as any); + qs.forEach((value, key) => url.searchParams.set(key, value)); + } else { + init = { body: new FormData(el), method: lowerMethod }; + } + + maybeUpdateHistory(url); + await fetchPartials(url, init); + } + } +}); + +function updateLinks(url: URL) { + document.querySelectorAll("a").forEach((link) => { + const match = matchesUrl(url.pathname, link.href); + + if (match === UrlMatchKind.Current) { + link.setAttribute(DATA_CURRENT, "true"); + link.setAttribute("aria-current", "page"); + link.removeAttribute(DATA_ANCESTOR); + } else if (match === UrlMatchKind.Ancestor) { + link.setAttribute(DATA_ANCESTOR, "true"); + link.setAttribute("aria-current", "true"); + link.removeAttribute(DATA_CURRENT); + } else { + link.removeAttribute(DATA_CURRENT); + link.removeAttribute(DATA_ANCESTOR); + link.removeAttribute("aria-current"); + } + }); +} + +async function fetchPartials(url: URL, init: RequestInit = {}) { + init.redirect = "follow"; + url.searchParams.set(PARTIAL_SEARCH_PARAM, "true"); + const res = await fetch(url, init); + await applyPartials(res); +} + +interface PartialReviveCtx { + foundPartials: number; +} + +/** + * Apply partials from a HTML response + */ +export async function applyPartials(res: Response): Promise { + const contentType = res.headers.get("Content-Type"); + if (contentType !== "text/html; charset=utf-8") { + throw new Error(`Unable to process partial response.`); + } + + const id = res.headers.get("X-Fresh-Id"); + + const resText = await res.text(); + const doc = new DOMParser().parseFromString(resText, "text/html") as Document; + + const state = doc.querySelector(`#__FRSH_STATE_${id}`); + let allProps: DeserializedProps = []; + if (state !== null) { + const json = JSON.parse(state.textContent!) as PartialStateJson; + const promises: Promise[] = []; + + allProps = parse(json.props, CUSTOM_PARSER); + + for (let i = 0; i < json.islands.length; i++) { + const island = json.islands[i]; + promises.push( + import(island.chunk).then((mod) => { + ISLAND_REGISTRY.set(island.name, mod[island.exportName]); + }), + ); + } + + await Promise.all(promises); + } + + const ctx: PartialReviveCtx = { + foundPartials: 0, + }; + + if (doc.title) { + document.title = doc.title; + } + + // Needs to be converted to an array otherwise somehow -tags + // are missing. + Array.from(doc.head.childNodes).forEach((childNode) => { + const child = childNode as HTMLElement; + + if (child.nodeName === "TITLE") return; + if (child.nodeName === "META") { + const meta = child as HTMLMetaElement; + + // Ignore charset which is usually set site wide anyway + if (meta.hasAttribute("charset")) return; + + const name = meta.name; + if (name !== "") { + const existing = document.head.querySelector(`meta[name="${name}"]`) as + | HTMLMetaElement + | null; + if (existing !== null) { + if (existing.content !== meta.content) { + existing.content = meta.content; + } + } else { + document.head.appendChild(meta); + } + } else { + const property = child.getAttribute("property"); + const existing = document.head.querySelector( + `meta[property="${property}"]`, + ) as HTMLMetaElement | null; + if (existing !== null) { + if (existing.content !== meta.content) { + existing.content = meta.content; + } + } else { + document.head.appendChild(meta); + } + } + } else if (child.nodeName === "LINK") { + const link = child as HTMLLinkElement; + if (link.rel === "modulepreload") return; + if (link.rel === "stylesheet") { + // The `href` attribute may be root relative. This ensures + // that they both have the same format + const existing = Array.from(document.head.querySelectorAll("link")) + .find((existingLink) => existingLink.href === link.href); + if (existing === undefined) { + document.head.appendChild(link); + } + } + } else if (child.nodeName === "SCRIPT") { + const script = child as HTMLScriptElement; + if (script.src === `${INTERNAL_PREFIX}/fresh-runtime.js`) return; + // TODO: What to do with script tags? + } else if (child.nodeName === "STYLE") { + const style = child as HTMLStyleElement; + // TODO: Do we need a smarter merging strategy? + // Don't overwrie existing style sheets that are flagged as unique + if (style.id === "") { + document.head.appendChild(style); + } + } + }); + + revivePartials(ctx, allProps, doc.body); + + if (ctx.foundPartials === 0) { + throw new NoPartialsError( + `Found no partials in HTML response. Please make sure to render at least one partial. Requested url: ${res.url}`, + ); + } +} + +function revivePartials( + ctx: PartialReviveCtx, + allProps: DeserializedProps, + node: Element, +) { + let startNode = null; + let sib: ChildNode | null = node.firstChild; + let partialCount = 0; + let partialName = ""; + let partialKey = ""; + let partialMode = PartialMode.Replace; + while (sib !== null) { + if (isCommentNode(sib)) { + const comment = sib.data; + const parts = comment.split(":"); + if (parts[0] === "frsh") { + sib = maybeHideMarker(sib); + } + + if (parts[0] === "frsh" && parts[1] === "partial") { + if (++partialCount === 1) { + startNode = sib; + partialName = parts[2]; + partialMode = +parts[3] as PartialMode; + partialKey = parts[4]; + } + } else if (comment === "/frsh:partial") { + ctx.foundPartials++; + + // Skip hydrating nested partials, only hydrate the outer one + if (--partialCount > 0) { + sib = sib.nextSibling; + continue; + } + + // Create a fake DOM node that spans the partial we discovered. + // We need to include the partial markers itself for _walkInner + // to register them. + const container = createRootFragment( + node, + startNode as Comment, + sib as Comment, + ); + + const root = h(PartialComp, { + key: partialKey !== "" ? partialKey : undefined, + name: partialName, + mode: partialMode, + children: null, + }); + domToVNode( + allProps, + [root], + [Marker.Partial], + container, + sib as Comment, + ); + + const instance = ACTIVE_PARTIALS.get(partialName); + if (instance === undefined) { + console.warn(`Partial "${partialName}" not found. Skipping...`); + // Partial doesn't exist on the current page + } else { + if (partialMode === PartialMode.Replace) { + instance.props.children = root.props.children; + } else if (partialMode === PartialMode.Append) { + const active = ACTIVE_PARTIALS.get(partialName); + if (active !== undefined) { + copyOldChildren(instance.props, active.props.children); + + (instance.props.children as ComponentChildren[]).push( + root.props.children, + ); + } else { + instance.props.children = root.props.children; + } + } else if (partialMode === PartialMode.Prepend) { + const active = ACTIVE_PARTIALS.get(partialName); + if (active !== undefined) { + copyOldChildren(instance.props, active.props.children); + + (instance.props.children as ComponentChildren[]).unshift( + root.props.children, + ); + } else { + instance.props.children = root.props.children; + } + } + instance.setState({}); + } + } + } else if (partialCount === 0 && isElementNode(sib)) { + // Do not recurse if we know that we are inisde a partial + revivePartials(ctx, allProps, sib); + } + + sib = sib.nextSibling; + } +} diff --git a/src/runtime/polyfills.ts b/src/runtime/client/polyfills.ts similarity index 100% rename from src/runtime/polyfills.ts rename to src/runtime/client/polyfills.ts diff --git a/src/runtime/client/preact_hooks_client.ts b/src/runtime/client/preact_hooks_client.ts new file mode 100644 index 00000000000..a4ff983aacd --- /dev/null +++ b/src/runtime/client/preact_hooks_client.ts @@ -0,0 +1,9 @@ +import { options } from "preact"; +import { assetHashingHook } from "../shared_internal.tsx"; +import { BUILD_ID } from "../build_id.ts"; + +const oldVNodeHook = options.vnode; +options.vnode = (vnode) => { + assetHashingHook(vnode, BUILD_ID); + oldVNodeHook?.(vnode); +}; diff --git a/src/runtime/client/reviver.ts b/src/runtime/client/reviver.ts new file mode 100644 index 00000000000..fdec9ea8c2f --- /dev/null +++ b/src/runtime/client/reviver.ts @@ -0,0 +1,567 @@ +import { + Component, + type ComponentChildren, + type ComponentType, + Fragment, + h, + render, + type VNode, +} from "preact"; +import { type CustomParser, parse } from "../../jsonify/parse.ts"; +import { signal } from "@preact/signals"; +import { DATA_FRESH_KEY, PartialMode } from "../shared_internal.tsx"; + +const enum RootKind { + Island, + Partial, +} + +interface IslandReq { + kind: RootKind.Island; + name: string; + propsIdx: number; + key: string | null; + start: Comment | Text; + end: Comment | Text | null; +} +interface PartialReq { + kind: RootKind.Partial; + name: string; + key: string | null; + start: Comment | Text; + end: Comment | Text | null; +} + +interface ReviveContext { + roots: Array; + stack: Array; + slots: Map; + slotIdStack: number[]; +} + +interface SlotRef { + kind: typeof SLOT_SYMBOL; + name: string; + id: number; +} +const SLOT_SYMBOL = Symbol.for("_FRESH_SLOT"); +function isSlotRef(x: unknown): x is SlotRef { + return x !== null && typeof x === "object" && "kind" in x && + x.kind === SLOT_SYMBOL; +} + +export type DeserializedProps = { + props: Record; + slots: SlotRef[]; +}[]; + +export const ACTIVE_PARTIALS = new Map(); + +export class PartialComp extends Component< + { children?: ComponentChildren; mode: PartialMode; name: string } +> { + componentDidMount() { + ACTIVE_PARTIALS.set(this.props.name, this); + } + + render() { + return this.props.children; + } +} +PartialComp.displayName = "Partial"; + +export function revive( + props: Record, + component: ComponentType, + container: HTMLElement, + slots: ReviveContext["slots"], + allProps: DeserializedProps, +) { + const _render = () => { + for (const propName in props) { + const value = props[propName]; + if (isSlotRef(value)) { + const marker = slots.get(value.id); + if (marker !== undefined) { + const root = h(Fragment, null); + const slotContainer = createRootFragment( + container, + marker.start, + marker.end!, + ); + domToVNode( + allProps, + [root], + [Marker.Slot], + slotContainer, + marker.end!, + ); + props[propName] = root; + } else { + const template = document.querySelector( + `#frsh-${value.id}-${value.name}`, + ) as HTMLTemplateElement | null; + if (template !== null) { + const root = h(Fragment, null); + domToVNode(allProps, [root], [Marker.Slot], template.content, null); + props[propName] = root; + } + } + } + } + + // TODO: explore hydrate? + render(h(component, props), container as unknown as HTMLElement); + }; + + // deno-lint-ignore no-window + "scheduler" in window + // `scheduler.postTask` is async but that can easily + // fire in the background. We don't want waiting for + // the hydration of an island block us. + // @ts-ignore scheduler API is not in types yet + ? scheduler!.postTask(_render) + : setTimeout(_render, 0); +} + +export const ISLAND_REGISTRY = new Map(); + +export const CUSTOM_PARSER: CustomParser = { + Signal: (value: unknown) => signal(value), + Slot: (value: { name: string; id: number }): SlotRef => { + return { kind: SLOT_SYMBOL, name: value.name, id: value.id }; + }, +}; + +export function createReviveCtx(): ReviveContext { + return { + roots: [], + stack: [], + slots: new Map(), + slotIdStack: [], + }; +} + +export function boot( + initialIslands: Record, + islandProps: string, +) { + const ctx = createReviveCtx(); + _walkInner(ctx, document.body); + + const keys = Object.keys(initialIslands); + for (let i = 0; i < keys.length; i++) { + const name = keys[i]; + ISLAND_REGISTRY.set(name, initialIslands[name]); + } + + const allProps = parse( + islandProps, + CUSTOM_PARSER, + ); + + for (let i = 0; i < ctx.roots.length; i++) { + const root = ctx.roots[i]; + + const container = createRootFragment( + // deno-lint-ignore no-explicit-any + root.start.parentNode as any, + root.start, + root.end!, + ); + + if (root.kind === RootKind.Island) { + const props = allProps[root.propsIdx].props; + const component = ISLAND_REGISTRY.get(root.name)!; + + revive(props, component, container, ctx.slots, allProps); + } else if (root.kind === RootKind.Partial) { + const props: Record = { + name: root.name, + children: null, + }; + + const domRoot = h(Fragment, null); + domToVNode(allProps, [domRoot], [Marker.Partial], container, root.end!); + props.children = domRoot.props.children; + + // deno-lint-ignore no-explicit-any + revive(props, PartialComp as any, container, ctx.slots, allProps); + } + } +} + +const SHOW_MARKERS = false; + +interface FreshMarker extends Text { + _frshMarker: string; +} + +export function isFreshMarkerText(node: Node): node is FreshMarker { + return node.nodeType === Node.TEXT_NODE && + // deno-lint-ignore no-explicit-any + typeof (node as any)._frshMarker === "string"; +} + +/** + * Replace comment markers with empty text nodes to hide them + * in DevTools. This is done to avoid user confusion. + */ +export function maybeHideMarker(marker: Comment): Comment | Text { + if (SHOW_MARKERS) return marker; + const text = new Text("") as FreshMarker; + text._frshMarker = marker.data; + marker.parentNode!.insertBefore(text, marker); + marker.remove(); + return text; +} + +function _walkInner( + ctx: ReviveContext, + node: Node | Comment, +) { + if (isElementNode(node)) { + // No need to traverse into " }; - assertEquals( - htmlEscapeJsonString(JSON.stringify(evilObj)), - '{"evil":"\\u003cscript\\u003e\\u003c/script\\u003e"}', - ); -}); - -Deno.test("with angle brackets should parse back", () => { - const evilObj = { evil: "" }; - assertEquals( - JSON.parse(htmlEscapeJsonString(JSON.stringify(evilObj))), - evilObj, - ); -}); diff --git a/src/server/init_safe_deps.ts b/src/server/init_safe_deps.ts deleted file mode 100644 index 6a2a1283c64..00000000000 --- a/src/server/init_safe_deps.ts +++ /dev/null @@ -1,34 +0,0 @@ -// This file includes dependencies that are safe to use even -// when the user has no `deno.json` in their project folder. -// This commonly occurs when the user is bootstrapping a new -// project. - -export { - isIdentifierChar, - isIdentifierStart, -} from "https://esm.sh/@babel/helper-validator-identifier@7.22.20"; -import { - isIdentifierChar, - isIdentifierStart, -} from "https://esm.sh/@babel/helper-validator-identifier@7.22.20"; - -export function stringToIdentifier(str: string): string { - let ident = ""; - for (let i = 0; i < str.length; i++) { - const char = str.charCodeAt(i); - if (i === 0 && !isIdentifierStart(char)) { - ident += "_"; - if (isIdentifierChar(char)) { - ident += str[i]; - } - } else if (!isIdentifierChar(char)) { - if (ident[ident.length - 1] !== "_") { - ident += "_"; - } - } else if (ident[ident.length - 1] !== "_" || str[i] !== "_") { - ident += str[i]; - } - } - - return ident; -} diff --git a/src/server/mod.ts b/src/server/mod.ts deleted file mode 100644 index d0fcb3d3b2c..00000000000 --- a/src/server/mod.ts +++ /dev/null @@ -1,119 +0,0 @@ -import { ServerContext } from "./context.ts"; -export type { FromManifestConfig, FromManifestOptions } from "./context.ts"; -export { STATUS_CODE } from "./deps.ts"; -import { - ErrorHandler, - FreshConfig, - Handler, - Handlers, - IslandModule, - LayoutConfig, - MiddlewareModule, - RouteConfig, - ServeHandlerInfo, - UnknownHandler, -} from "./types.ts"; -import { startServer } from "./boot.ts"; -export { - defineApp, - defineConfig, - defineLayout, - defineRoute, -} from "./defines.ts"; -export type { - AppContext, - AppProps, - DenoConfig, - ErrorHandler, - ErrorHandlerContext, - ErrorPageProps, - FreshConfig, - FreshContext, - FreshOptions, - Handler, - HandlerContext, - Handlers, - LayoutConfig, - LayoutContext, - LayoutProps, - MiddlewareHandler, - MiddlewareHandlerContext, - MultiHandler, - PageProps, - Plugin, - PluginAsyncRenderContext, - PluginAsyncRenderFunction, - PluginIslands, - PluginMiddleware, - PluginRenderContext, - PluginRenderFunction, - PluginRenderFunctionResult, - PluginRenderResult, - PluginRenderScripts, - PluginRenderStyleTag, - PluginRoute, - RenderFunction, - ResolvedFreshConfig, - RouteConfig, - RouteContext, - RouterOptions, - ServeHandlerInfo, - StartOptions, - UnknownHandler, - UnknownHandlerContext, - UnknownPageProps, -} from "./types.ts"; -export { RenderContext } from "./render.ts"; -export type { InnerRenderFunction } from "./render.ts"; -export type { DestinationKind } from "./router.ts"; - -export interface Manifest { - routes: Record< - string, - { - // Use a more loose route definition type because - // TS has trouble inferring normal vs aync functions. It cannot infer based on function arity - default?: ( - // deno-lint-ignore no-explicit-any - propsOrRequest: any, - // deno-lint-ignore no-explicit-any - ctx: any, - // deno-lint-ignore no-explicit-any - ) => Promise | any; - handler?: - // deno-lint-ignore no-explicit-any - | Handler - // deno-lint-ignore no-explicit-any - | Handlers - | UnknownHandler - | ErrorHandler; - config?: RouteConfig | LayoutConfig; - } | MiddlewareModule - >; - islands: Record; - baseUrl: string; -} - -export { ServerContext }; - -export async function createHandler( - manifest: Manifest, - config: FreshConfig = {}, -): Promise< - (req: Request, connInfo?: ServeHandlerInfo) => Promise -> { - const ctx = await ServerContext.fromManifest(manifest, config); - return ctx.handler(); -} - -export async function start(manifest: Manifest, config: FreshConfig = {}) { - const ctx = await ServerContext.fromManifest(manifest, { - ...config, - dev: false, - }); - const realConfig = config.server ?? config; - await startServer(ctx.handler(), { - ...realConfig, - basePath: config?.router?.basePath ?? "", - }); -} diff --git a/src/server/render.ts b/src/server/render.ts deleted file mode 100644 index 6feed668cf8..00000000000 --- a/src/server/render.ts +++ /dev/null @@ -1,399 +0,0 @@ -import { h, VNode } from "preact"; -import { - AppModule, - AsyncLayout, - AsyncRoute, - ErrorPage, - FreshContext, - LayoutRoute, - PageProps, - Plugin, - PluginRenderFunctionResult, - PluginRenderResult, - RenderFunction, - Route, - UnknownPage, -} from "./types.ts"; -import { NONE, UNSAFE_INLINE } from "../runtime/csp.ts"; -import { ContentSecurityPolicy } from "../runtime/csp.ts"; -import { RenderState } from "./rendering/state.ts"; -import { renderHtml, renderOuterDocument } from "./rendering/template.tsx"; -import { renderFreshTags } from "./rendering/fresh_tags.tsx"; -import { DEV_ERROR_OVERLAY_URL } from "./constants.ts"; -import { colors } from "./deps.ts"; -import { withBase } from "./router.ts"; - -export const DEFAULT_RENDER_FN: RenderFunction = (_ctx, render) => { - render(); -}; - -export interface RenderOptions { - request: Request; - context: FreshContext; - route: Route | UnknownPage | ErrorPage; - plugins: Plugin[]; - app: AppModule; - layouts: LayoutRoute[]; - imports: string[]; - dependenciesFn: (path: string) => string[]; - renderFn: RenderFunction; - codeFrame?: string; - lang?: string; -} - -export type InnerRenderFunction = () => string; - -export class RenderContext { - #id: string; - #state: Map = new Map(); - #styles: string[] = []; - #url: URL; - #route: string; - #lang: string; - - constructor(id: string, url: URL, route: string, lang: string) { - this.#id = id; - this.#url = url; - this.#route = route; - this.#lang = lang; - } - - /** A unique ID for this logical JIT render. */ - get id(): string { - return this.#id; - } - - /** - * State that is persisted between multiple renders with the same render - * context. This is useful because one logical JIT render could have multiple - * preact render passes due to suspense. - */ - get state(): Map { - return this.#state; - } - - /** - * All of the CSS style rules that should be inlined into the document. - * Adding to this list across multiple renders is supported (even across - * suspense!). The CSS rules will always be inserted on the client in the - * order specified here. - */ - get styles(): string[] { - return this.#styles; - } - - /** The URL of the page being rendered. */ - get url(): URL { - return this.#url; - } - - /** The route matcher (e.g. /blog/:id) that the request matched for this page - * to be rendered. */ - get route(): string { - return this.#route; - } - - /** The language of the page being rendered. Defaults to "en". */ - get lang(): string { - return this.#lang; - } - set lang(lang: string) { - this.#lang = lang; - } -} - -function defaultCsp() { - return { - directives: { defaultSrc: [NONE], styleSrc: [UNSAFE_INLINE] }, - reportOnly: false, - }; -} - -export function checkAsyncComponent( - component: unknown, -): component is AsyncRoute | AsyncLayout { - return typeof component === "function" && - component.constructor.name === "AsyncFunction"; -} - -/** - * This function renders out a page. Rendering is synchronous and non streaming. - * Suspense boundaries are not supported. - */ -export async function render( - opts: RenderOptions, -): Promise<[string, string, ContentSecurityPolicy | undefined] | Response> { - const component = opts.route.component; - - // Only inherit layouts up to the nearest root layout. - // Note that the route itself can act as the root layout. - let layouts = opts.layouts; - if (opts.route.inheritLayouts) { - let rootIdx = 0; - let layoutIdx = opts.layouts.length; - while (layoutIdx--) { - if (!opts.layouts[layoutIdx].inheritLayouts) { - rootIdx = layoutIdx; - break; - } - } - layouts = opts.layouts.slice(rootIdx); - } else { - layouts = []; - } - - const { params, data, state, error, url, basePath } = opts.context; - - const props: PageProps = { - basePath, - config: opts.context.config, - destination: opts.context.destination, - isPartial: opts.context.isPartial, - params, - error, - codeFrame: opts.context.codeFrame, - remoteAddr: opts.context.remoteAddr, - localAddr: opts.context.localAddr, - Component: () => null, - pattern: opts.context.pattern, - url, - route: opts.context.route, - data, - state, - }; - - const csp: ContentSecurityPolicy | undefined = opts.route.csp - ? defaultCsp() - : undefined; - if (csp) { - // Clear the csp - const newCsp = defaultCsp(); - csp.directives = newCsp.directives; - csp.reportOnly = newCsp.reportOnly; - } - - const ctx = new RenderContext( - crypto.randomUUID(), - url, - opts.route.pattern, - opts.lang ?? "en", - ); - - const context = opts.context; - - // Prepare render order - // deno-lint-ignore no-explicit-any - const renderStack: any[] = []; - // Check if appLayout is enabled - if ( - opts.route.appWrapper && - layouts.every((layout) => layout.appWrapper) - ) { - renderStack.push(opts.app.default); - } - for (let i = 0; i < layouts.length; i++) { - renderStack.push(layouts[i].component); - } - renderStack.push(component); - - // Build the final stack of component functions - const componentStack = new Array(renderStack.length).fill(null); - for (let i = 0; i < renderStack.length; i++) { - const fn = renderStack[i]; - if (!fn) continue; - - if (checkAsyncComponent(fn)) { - // Don't pass when it's the route component - const isRouteComponent = fn === component; - const componentCtx = isRouteComponent ? context : { - ...context, - Component() { - return h(componentStack[i + 1], props); - }, - }; - // deno-lint-ignore no-explicit-any - const res = await fn(opts.request, componentCtx as any); - - // Bail out of rendering if we returned a response - if (res instanceof Response) { - return res; - } - - const componentFn = () => res; - // Set displayName to make debugging easier - // deno-lint-ignore no-explicit-any - componentFn.displayName = (fn as any).displayName || fn.name; - componentStack[i] = componentFn; - } else { - componentStack[i] = () => { - return h(fn, { - ...props, - Component() { - return h(componentStack[i + 1], null); - }, - // deno-lint-ignore no-explicit-any - } as any); - }; - } - } - - // CAREFUL: Rendering is synchronous internally and all state - // should be managed through the `RenderState` instance. That - // ensures that each render request is associated with the same - // data. - const renderState = new RenderState( - crypto.randomUUID(), - { - url, - route: opts.route.pattern, - data, - state, - params, - basePath, - }, - componentStack, - csp, - error, - ); - - let bodyHtml: string | null = null; - - const syncPlugins = opts.plugins.filter((p) => p.render); - - const renderResults: [Plugin, PluginRenderResult][] = []; - - function renderSync(): PluginRenderFunctionResult { - const plugin = syncPlugins.shift(); - if (plugin) { - const res = plugin.render!({ render: renderSync }); - if (res === undefined) { - throw new Error( - `${plugin?.name}'s render hook did not return a PluginRenderResult object.`, - ); - } - renderResults.push([plugin, res]); - - if (res.htmlText !== undefined) { - bodyHtml = res.htmlText; - } - } else { - bodyHtml = renderHtml(renderState); - } - if (bodyHtml === null) { - throw new Error( - `The 'render' function was not called by ${plugin?.name}'s render hook.`, - ); - } - return { - htmlText: bodyHtml, - requiresHydration: renderState.encounteredIslands.size > 0, - }; - } - - const asyncPlugins = opts.plugins.filter((p) => p.renderAsync); - - let asyncRenderResponse: Response | undefined; - async function renderAsync(): Promise { - const plugin = asyncPlugins.shift(); - if (plugin) { - const res = await plugin.renderAsync!({ renderAsync }); - if (res === undefined) { - throw new Error( - `${plugin?.name}'s async render hook did not return a PluginRenderResult object.`, - ); - } - renderResults.push([plugin, res]); - if (bodyHtml === null) { - throw new Error( - `The 'renderAsync' function was not called by ${plugin?.name}'s async render hook.`, - ); - } - - if (res.htmlText !== undefined) { - bodyHtml = res.htmlText; - } - } else { - await opts.renderFn(ctx, () => renderSync().htmlText); - - if (bodyHtml === null) { - throw new Error( - `The 'render' function was not called by the legacy async render hook.`, - ); - } - } - return { - htmlText: bodyHtml, - requiresHydration: renderState.encounteredIslands.size > 0, - }; - } - - await renderAsync(); - if (renderState.error !== null) { - throw renderState.error; - } - - const idx = renderState.headVNodes.findIndex((vnode) => - vnode !== null && typeof vnode === "object" && "type" in vnode && - props !== null && vnode.type === "title" - ); - if (idx !== -1) { - renderState.docTitle = renderState.headVNodes[idx] as VNode< - { children: string } - >; - renderState.headVNodes.splice(idx, 1); - } - - if (asyncRenderResponse !== undefined) { - return asyncRenderResponse; - } - - // Includes everything inside `` - bodyHtml = bodyHtml as unknown as string; - - // Create Fresh script + style tags - const result = renderFreshTags(renderState, { - bodyHtml, - imports: opts.imports, - csp, - dependenciesFn: opts.dependenciesFn, - styles: ctx.styles, - pluginRenderResults: renderResults, - basePath, - }); - - // Append error overlay in dev mode - if (opts.context.config.dev) { - const devErrorUrl = withBase(DEV_ERROR_OVERLAY_URL, basePath); - if (error !== undefined && url.pathname !== devErrorUrl) { - const url = new URL(devErrorUrl, "https://localhost/"); - if (error instanceof Error) { - let message = error.message; - const idx = message.indexOf("\n"); - if (idx > -1) message = message.slice(0, idx); - url.searchParams.append("message", message); - if (error.stack) { - const stack = colors.stripAnsiCode(error.stack); - url.searchParams.append("stack", stack); - } - } else { - url.searchParams.append("message", String(error)); - } - if (opts.codeFrame) { - const codeFrame = colors.stripAnsiCode(opts.codeFrame); - url.searchParams.append("code-frame", codeFrame); - } - - result.bodyHtml += - ``; - } - } - - // Render outer document up to `` - const html = renderOuterDocument(renderState, { - bodyHtml: result.bodyHtml, - preloads: [...result.preloadSet], - moduleScripts: result.moduleScripts, - lang: ctx.lang, - }); - return [html, renderState.renderUuid, csp]; -} diff --git a/src/server/rendering/fresh_tags.tsx b/src/server/rendering/fresh_tags.tsx deleted file mode 100644 index 40b2fd0034d..00000000000 --- a/src/server/rendering/fresh_tags.tsx +++ /dev/null @@ -1,205 +0,0 @@ -import { bundleAssetUrl } from "../constants.ts"; -import { RenderState } from "./state.ts"; -import { htmlEscapeJsonString } from "../htmlescape.ts"; -import { serialize } from "../serializer.ts"; -import { - Plugin, - PluginRenderLink, - PluginRenderResult, - PluginRenderStyleTag, -} from "../types.ts"; -import { ContentSecurityPolicy, nonce } from "../../runtime/csp.ts"; -import { h } from "preact"; - -export type SerializedState = [islands: unknown[], plugins: unknown[]]; - -export function renderFreshTags( - renderState: RenderState, - opts: { - bodyHtml: string; - csp?: ContentSecurityPolicy; - imports: string[]; - randomNonce?: string; - dependenciesFn: (path: string) => string[]; - styles: string[]; - pluginRenderResults: [Plugin, PluginRenderResult][]; - basePath: string; - }, -) { - const { isPartial } = renderState; - - if (opts.csp) { - opts.csp.directives.scriptSrc = [ - ...opts.csp.directives.scriptSrc ?? [], - nonce(renderState.getNonce()), - ]; - } - - const moduleScripts: [string, string][] = []; - for (const url of opts.imports) { - moduleScripts.push([url, renderState.getNonce()]); - } - - const preloadSet = new Set(); - function addImport(path: string): string { - const url = opts.basePath + bundleAssetUrl(`/${path}`); - if (!isPartial) { - preloadSet.add(url); - for (const depPath of opts.dependenciesFn(path)) { - const url = bundleAssetUrl(`/${depPath}`); - preloadSet.add(url); - } - } - return url; - } - - const state: SerializedState = [ - renderState.islandProps, - [], - ]; - const styleTags: PluginRenderStyleTag[] = []; - const linkTags: PluginRenderLink[] = []; - const pluginScripts: [string, string, number][] = []; - - for (const [plugin, res] of opts.pluginRenderResults) { - for (const hydrate of res.scripts ?? []) { - const i = state[1].push(hydrate.state) - 1; - pluginScripts.push([plugin.name, hydrate.entrypoint, i]); - } - styleTags.splice(styleTags.length, 0, ...res.styles ?? []); - linkTags.splice(linkTags.length, 0, ...res.links ?? []); - } - - // The inline script that will hydrate the page. - let script = ""; - - // Serialize the state into the `; - - hasSignals = res.hasSignals; - requiresDeserializer = res.requiresDeserializer; - - if (res.requiresDeserializer) { - const url = addImport("deserializer.js"); - script += `import { deserialize } from "${url}";`; - } - if (res.hasSignals) { - const url = addImport("signals.js"); - script += `import { signal } from "${url}";`; - } - script += `const ST = document.getElementById("${stateId}").textContent;`; - script += `const STATE = `; - if (res.requiresDeserializer) { - if (res.hasSignals) { - script += `deserialize(ST, signal);`; - } else { - script += `deserialize(ST);`; - } - } else { - script += `JSON.parse(ST).v;`; - } - } - - // Then it imports all plugin scripts and executes them (with their respective - // state). - if (pluginScripts.length > 0) { - // Use `reportError` if available, otherwise throw in a different event - // loop tick to avoid halting the current script. - script += - `function runPlugin(fn,args){try{fn(args)}catch(err){setTimeout(() => {throw err})}}`; - } - for (const [pluginName, entrypoint, i] of pluginScripts) { - const url = addImport(`plugin-${pluginName}-${entrypoint}.js`); - script += `import p${i} from "${url}";runPlugin(p${i},STATE[1][${i}]);`; - } - - const needsMainScript = renderState.encounteredIslands.size > 0 || - renderState.partialCount > 0; - if (needsMainScript) { - // Load the main.js script - const url = addImport("main.js"); - script += `import { revive } from "${url}";`; - } - - // Finally, it loads all island scripts and hydrates the islands using the - // reviver from the "main" script. - let islandRegistry = ""; - const islandMapping: Record = {}; - if (renderState.encounteredIslands.size > 0) { - // Prepare the inline script that loads and revives the islands - for (const island of renderState.encounteredIslands) { - const url = addImport(`island-${island.name}.js`); - script += island.exportName === "default" - ? `import ${island.name}_${island.exportName} from "${url}";` - : `import { ${island.exportName} as ${island.name}_${island.exportName} } from "${url}";`; - islandRegistry += `${island.id}:${island.name}_${island.exportName},`; - islandMapping[island.id] = { - export: island.exportName, - url, - }; - } - } - - // Always revive to detect partials - if (needsMainScript) { - script += `const propsArr = typeof STATE !== "undefined" ? STATE[0] : [];`; - script += `revive({${islandRegistry}}, propsArr);`; - } - - // Append the inline script. - if (isPartial && Object.keys(islandMapping).length > 0) { - const escapedData = htmlEscapeJsonString( - JSON.stringify({ - islands: islandMapping, - signals: hasSignals ? addImport("signals.js") : null, - deserializer: requiresDeserializer - ? addImport("deserializer.js") - : null, - }), - ); - const nonce = renderState.csp ? ` nonce="${renderState.getNonce()}"` : ""; - opts.bodyHtml += - ``; - } - if (script !== "") { - opts.bodyHtml += - ``; - } - - if (opts.styles.length > 0) { - const node = h("style", { - id: "__FRSH_STYLE", - dangerouslySetInnerHTML: { __html: opts.styles.join("\n") }, - }); - - renderState.headVNodes.splice(0, 0, node); - } - - for (const style of styleTags) { - const node = h("style", { - id: style.id, - media: style.media, - dangerouslySetInnerHTML: { __html: style.cssText }, - }); - renderState.headVNodes.splice(0, 0, node); - } - - for (const link of linkTags) { - const node = h("link", link); - renderState.headVNodes.splice(0, 0, node); - } - - return { bodyHtml: opts.bodyHtml, preloadSet, moduleScripts }; -} diff --git a/src/server/rendering/preact_hooks.ts b/src/server/rendering/preact_hooks.ts deleted file mode 100644 index d935daddf33..00000000000 --- a/src/server/rendering/preact_hooks.ts +++ /dev/null @@ -1,489 +0,0 @@ -import { - Component, - type ComponentChildren, - ComponentType, - Fragment, - h, - isValidElement, - type Options as PreactOptions, - options as preactOptions, - type VNode, -} from "preact"; -import { assetHashingHook } from "../../runtime/utils.ts"; -import { Partial, PartialProps } from "../../runtime/Partial.tsx"; -import { join, renderToString, SEPARATOR } from "../deps.ts"; -import { RenderState } from "./state.ts"; -import { Island } from "../types.ts"; -import { - CLIENT_NAV_ATTR, - DATA_KEY_ATTR, - LOADING_ATTR, - PartialMode, -} from "../../constants.ts"; -import { setActiveUrl } from "../../runtime/active_url.ts"; -import { withBase } from "../router.ts"; - -// See: https://github.com/preactjs/preact/blob/7748dcb83cedd02e37b3713634e35b97b26028fd/src/internal.d.ts#L3C1-L16 -enum HookType { - useState = 1, - useReducer = 2, - useEffect = 3, - useLayoutEffect = 4, - useRef = 5, - useImperativeHandle = 6, - useMemo = 7, - useCallback = 8, - useContext = 9, - useErrorBoundary = 10, - // Not a real hook, but the devtools treat is as such - useDebugvalue = 11, -} - -// These hooks are long stable, but when we originally added them we -// weren't sure if they should be public. -interface AdvancedPreactOptions extends PreactOptions { - /** Attach a hook that is invoked after a tree was mounted or was updated. */ - __c?(vnode: VNode, commitQueue: Component[]): void; - /** Attach a hook that is invoked before a vnode has rendered. */ - __r?(vnode: VNode): void; - errorBoundaries?: boolean; - /** before diff hook */ - __b?(vnode: VNode): void; - /** Attach a hook that is invoked before a hook's state is queried. */ - __h?(component: Component, index: number, type: HookType): void; -} -const options = preactOptions as AdvancedPreactOptions; - -// Enable error boundaries in Preact. -options.errorBoundaries = true; - -// Set up a preact option hook to track when vnode with custom functions are -// created. -let current: RenderState | null = null; -// Keep track of which component rendered which vnode. This allows us -// to detect when an island is rendered within another instead of being -// passed as children. -let ownerStack: VNode[] = []; -// Keep track of all available islands -const islandByComponent = new Map(); -export function setAllIslands(islands: Island[]) { - for (let i = 0; i < islands.length; i++) { - const island = islands[i]; - islandByComponent.set(island.component, island); - } -} - -export function setRenderState(state: RenderState | null): void { - if (current) current.clearTmpState(); - current = state; - ownerStack = state?.ownerStack ?? []; -} - -// Check if an older version of `preact-render-to-string` is used -const supportsUnstableComments = renderToString(h(Fragment, { - // @ts-ignore unstable features not supported in types - UNSTABLE_comment: "foo", -}) as VNode) !== ""; - -if (!supportsUnstableComments) { - console.warn( - "⚠️ Found old version of 'preact-render-to-string'. Please upgrade it to >=6.1.0", - ); -} - -/** - * Wrap a node with comment markers in the HTML - */ -function wrapWithMarker(vnode: ComponentChildren, markerText: string) { - // Newer versions of preact-render-to-string allow you to render comments - if (supportsUnstableComments) { - return h( - Fragment, - null, - h(Fragment, { - // @ts-ignore unstable property is not typed - UNSTABLE_comment: markerText, - }), - vnode, - h(Fragment, { - // @ts-ignore unstable property is not typed - UNSTABLE_comment: "/" + markerText, - }), - ); - } else { - return h( - `!--${markerText}--`, - null, - vnode, - ); - } -} - -/** - * Whenever a slot (=jsx children) is rendered, remove this from the slot - * tracking Set. After everything was rendered we'll know which slots - * weren't and can send them down to the client - */ -function SlotTracker( - props: { id: string; children?: ComponentChildren }, -): VNode { - current?.slots.delete(props.id); - // deno-lint-ignore no-explicit-any - return props.children as any; -} - -/** - * Copy props but exclude children - */ -function excludeChildren(props: Record) { - const out: Record = {}; - for (const k in props) { - if (k !== "children") out[k] = props[k]; - } - return out; -} - -/** - * Check if the current component was rendered in an island - */ -function hasIslandOwner(current: RenderState, vnode: VNode): boolean { - let tmpVNode = vnode; - let owner; - while ((owner = current.owners.get(tmpVNode)) !== undefined) { - if (islandByComponent.has(owner.type as ComponentType)) { - return true; - } - tmpVNode = owner; - } - - return false; -} - -function encodePartialMode(mode: PartialProps["mode"]): PartialMode { - if (mode === "replace") return PartialMode.REPLACE; - else if (mode === "append") return PartialMode.APPEND; - else if (mode === "prepend") return PartialMode.PREPEND; - throw new Error(`Unknown partial mode "${mode}"`); -} - -const patched = new WeakSet(); - -const oldVNodeHook = options.vnode; -const oldDiff = options.__b; -const oldDiffed = options.diffed; -const oldRender = options.__r; -const oldHook = options.__h; - -options.vnode = (vnode) => { - assetHashingHook(vnode); - - // Work around `preact/debug` string event handler error which - // errors when an event handler gets a string. This makes sense - // on the client where this is a common vector for XSS. On the - // server when the string was not created through concatenation - // it is fine. Internally, `preact/debug` only checks for the - // lowercase variant. - if (typeof vnode.type === "string") { - const props = vnode.props as Record; - for (const key in props) { - const value = props[key]; - if (key.startsWith("on") && typeof value === "string") { - delete props[key]; - props["ON" + key.slice(2)] = value; - } - } - // Don't do key preservation for nodes in . - if ( - vnode.key && vnode.type !== "meta" && vnode.type !== "title" && - vnode.type !== "style" && vnode.type !== "script" && vnode.type !== "link" - ) { - props[DATA_KEY_ATTR] = vnode.key; - } - - if (props[LOADING_ATTR]) { - // Avoid automatic signals unwrapping - props[LOADING_ATTR] = { value: props[LOADING_ATTR] }; - } - - if (typeof props[CLIENT_NAV_ATTR] === "boolean") { - props[CLIENT_NAV_ATTR] = props[CLIENT_NAV_ATTR] ? "true" : "false"; - } - - if (typeof props.href === "string") { - props.href = withBase(props.href, current?.basePath); - } - - if (typeof props.src === "string") { - props.src = withBase(props.src, current?.basePath); - } - - srcsetRewrite: - if (typeof props.srcset === "string") { - // Bail out on complex syntax that's too complicated for now - if (props.srcset.includes("(")) break srcsetRewrite; - - const parts = props.srcset.split(","); - const out: string[] = []; - for (const part of parts) { - const trimmed = part.trimStart(); - if (trimmed === "") break srcsetRewrite; - - let urlEnd = trimmed.indexOf(" "); - if (urlEnd === -1) urlEnd = trimmed.length; - - const leadingWhitespace = part.length - trimmed.length; - const leading = part.substring(0, leadingWhitespace); - const url = trimmed.substring(0, urlEnd); - const trailing = trimmed.substring(urlEnd); - - if (url.startsWith("/") && current?.basePath) { - const joinedPath = join("/", current.basePath, url).replaceAll( - SEPARATOR, - "/", - ); - out.push(leading + joinedPath + trailing); - } else { - out.push(part); - } - } - props.srcset = out.join(","); - } - } else if ( - current && typeof vnode.type === "function" && vnode.type !== Fragment && - ownerStack.length > 0 - ) { - current.owners.set(vnode, ownerStack[ownerStack.length - 1]); - } - - if (oldVNodeHook) oldVNodeHook(vnode); -}; - -options.__b = (vnode: VNode>) => { - // Add CSP nonce to inline script tags - if (typeof vnode.type === "string" && vnode.type === "script") { - if (!vnode.props.nonce) { - vnode.props.nonce = current!.getNonce(); - } - } - - if ( - current && current.renderingUserTemplate - ) { - // Internally rendering happens in two phases. This is done so - // that the `` component works. When we do the first render - // we cache all attributes on ``, `` + its children, and - // ``. When doing so, we'll replace the tags with a Fragment node - // so that they don't end up in the rendered HTML. Effectively this - // means we'll only serialize the contents of ``. - // - // After that render is finished we know all additional - // meta tags that were inserted via `` and all islands that - // we can add as preloads. Then we do a second render of the outer - // HTML tags with the updated value and merge in the HTML generate by - // the first render into `` directly. - if ( - typeof vnode.type === "string" - ) { - if (vnode.type === "html") { - current.renderedHtmlTag = true; - current.docHtml = excludeChildren(vnode.props); - vnode.type = Fragment; - } else if (vnode.type === "head") { - current.docHead = excludeChildren(vnode.props); - current.headChildren = true; - vnode.type = Fragment; - vnode.props = { - __freshHead: true, - children: vnode.props.children, - }; - } else if (vnode.type === "body") { - current.docBody = excludeChildren(vnode.props); - vnode.type = Fragment; - } else if (current.headChildren) { - if (vnode.type === "title") { - current.docTitle = h("title", vnode.props); - vnode.props = { children: null }; - } else { - current.docHeadNodes.push({ - type: vnode.type, - props: vnode.props, - }); - } - vnode.type = Fragment; - vnode.props = { children: null }; - } else if (LOADING_ATTR in vnode.props) { - current.islandProps.push({ - [LOADING_ATTR]: vnode.props[LOADING_ATTR], - }); - vnode.props[LOADING_ATTR] = current.islandProps.length - 1; - } else if (vnode.type === "a") { - setActiveUrl(vnode, current.url.pathname); - } - } else if (typeof vnode.type === "function") { - // Detect island vnodes and wrap them with a marker - const island = islandByComponent.get(vnode.type); - patchIsland: - if ( - vnode.type !== Fragment && - island && - !patched.has(vnode) - ) { - current.islandDepth++; - - // Check if an island is rendered inside another island, not just - // passed as a child.In that case we treat it like a normal - // Component. Example: - // function Island() { - // return - // } - if (hasIslandOwner(current, vnode)) { - break patchIsland; - } - - // At this point we know that we need to patch the island. Mark the - // island in that we have already patched it. - const originalType = vnode.type; - patched.add(vnode); - - vnode.type = (props) => { - if (!current) return null; - - const { encounteredIslands, islandProps, slots } = current; - encounteredIslands.add(island); - - // Only passing children JSX to islands is supported for now - const id = islandProps.length; - if ("children" in props) { - let children = props.children; - - // Guard against passing objects as children to JSX - if ( - typeof children === "function" || ( - children !== null && typeof children === "object" && - !Array.isArray(children) && - !isValidElement(children) - ) - ) { - const name = originalType.displayName || originalType.name || - "Anonymous"; - - throw new Error( - `Invalid JSX child passed to island <${name} />. To resolve this error, pass the data as a standard prop instead.`, - ); - } - - const markerText = `frsh-slot-${island.id}:${id}:children`; - // @ts-ignore nonono - props.children = wrapWithMarker( - children, - markerText, - ); - slots.set(markerText, children); - children = props.children; - // deno-lint-ignore no-explicit-any - (props as any).children = h( - SlotTracker, - { id: markerText }, - children, - ); - } - - const child = h(originalType, props) as VNode; - patched.add(child); - islandProps.push(props); - - return wrapWithMarker( - child, - `frsh-${island.id}:${islandProps.length - 1}:${vnode.key ?? ""}`, - ); - }; - // deno-lint-ignore no-explicit-any - } else if (vnode.type === (Partial as any)) { - current.partialCount++; - current.partialDepth++; - if (hasIslandOwner(current, vnode)) { - throw new Error( - ` components cannot be used inside islands.`, - ); - } - const name = vnode.props.name as string; - if (current.encounteredPartials.has(name)) { - current.error = new Error( - `Duplicate partial name "${name}" found. The partial name prop is expected to be unique among partial components.`, - ); - } - current.encounteredPartials.add(name); - - const mode = encodePartialMode( - // deno-lint-ignore no-explicit-any - (vnode.props as any).mode ?? "replace", - ); - vnode.props.children = wrapWithMarker( - vnode.props.children, - `frsh-partial:${name}:${mode}:${vnode.key ?? ""}`, - ); - } else if ( - vnode.key && (current.islandDepth > 0 || current.partialDepth > 0) - ) { - const child = h(vnode.type, vnode.props); - vnode.type = Fragment; - vnode.props = { - children: wrapWithMarker(child, `frsh-key:${vnode.key}`), - }; - } - } - } - oldDiff?.(vnode); -}; -options.__r = (vnode) => { - if ( - typeof vnode.type === "function" && - vnode.type !== Fragment - ) { - ownerStack.push(vnode); - } - oldRender?.(vnode); -}; -options.diffed = (vnode: VNode>) => { - if (typeof vnode.type === "function") { - if (vnode.type !== Fragment) { - if (current) { - if (islandByComponent.has(vnode.type)) { - current.islandDepth--; - } else if (vnode.type === Partial as ComponentType) { - current.partialDepth--; - } - } - - ownerStack.pop(); - } else if (vnode.props.__freshHead) { - if (current) { - current.headChildren = false; - } - } - } - oldDiffed?.(vnode); -}; - -options.__h = (component, idx, type) => { - // deno-lint-ignore no-explicit-any - const vnode = (component as any).__v; - // Warn when using stateful hooks outside of islands - if ( - // Only error for stateful hooks for now. - (type === HookType.useState || type === HookType.useReducer) && current && - !islandByComponent.has(vnode.type) && !hasIslandOwner(current, vnode) && - !current.error - ) { - const name = HookType[type]; - const message = - `Hook "${name}" cannot be used outside of an island component.`; - const hint = type === HookType.useState - ? `\n\nInstead, use the "useSignal" hook to share state across islands.` - : ""; - - // Don't throw here because that messes up internal Preact state - current.error = new Error(message + hint); - } - oldHook?.(component, idx, type); -}; diff --git a/src/server/rendering/state.ts b/src/server/rendering/state.ts deleted file mode 100644 index 82e3a9225ec..00000000000 --- a/src/server/rendering/state.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { type ComponentChildren, type VNode } from "preact"; -import { Island } from "../types.ts"; -import { ContentSecurityPolicy } from "../../runtime/csp.ts"; -import { PARTIAL_SEARCH_PARAM } from "../../constants.ts"; - -export interface RenderStateRouteOptions { - url: URL; - route: string; - // deno-lint-ignore no-explicit-any - data?: any; - // deno-lint-ignore no-explicit-any - state?: any; - error?: unknown; - params: Record; - basePath: string; -} - -export class RenderState { - readonly renderUuid: string; - // deno-lint-ignore no-explicit-any - componentStack: any[]; - renderingUserTemplate = false; - encounteredIslands = new Set(); - islandProps: unknown[] = []; - slots = new Map(); - headChildren = false; - renderedHtmlTag = false; - // deno-lint-ignore no-explicit-any - docTitle: VNode | null = null; - docHtml: Record | null = null; - docHead: Record | null = null; - docBody: Record | null = null; - docHeadNodes: { type: string; props: Record }[] = []; - headVNodes: ComponentChildren[] = []; - // Route options - routeOptions: RenderStateRouteOptions; - csp: ContentSecurityPolicy | undefined; - // Preact state - ownerStack: VNode[] = []; - owners = new Map(); - #nonce = ""; - error: Error | null = null; - isPartial: boolean; - encounteredPartials = new Set(); - partialCount = 0; - partialDepth = 0; - islandDepth = 0; - url: URL; - basePath: string; - - constructor( - renderUuid: string, - routeOptions: RenderStateRouteOptions, - // deno-lint-ignore no-explicit-any - componentStack: any[], - csp?: ContentSecurityPolicy, - error?: unknown, - ) { - this.renderUuid = renderUuid; - this.routeOptions = routeOptions; - this.csp = csp; - this.componentStack = componentStack; - this.url = routeOptions.url; - this.isPartial = routeOptions.url.searchParams.has(PARTIAL_SEARCH_PARAM); - this.basePath = routeOptions.basePath; - - if (error) this.routeOptions.error = error; - } - - getNonce(): string { - if (this.#nonce === "") { - this.#nonce = crypto.randomUUID().replace(/-/g, ""); - } - return this.#nonce; - } - - clearTmpState() { - this.renderingUserTemplate = false; - this.ownerStack = []; - this.owners.clear(); - this.encounteredPartials.clear(); - } -} diff --git a/src/server/rendering/template.tsx b/src/server/rendering/template.tsx deleted file mode 100644 index 1755fc8c2e9..00000000000 --- a/src/server/rendering/template.tsx +++ /dev/null @@ -1,148 +0,0 @@ -import { RenderState } from "./state.ts"; -import { setRenderState } from "./preact_hooks.ts"; -import { renderToString } from "../deps.ts"; -import { - ComponentType, - Fragment, - h, - isValidElement, - toChildArray, - VNode, -} from "preact"; -import { HEAD_CONTEXT } from "../../runtime/head.ts"; -import { CSP_CONTEXT } from "../../runtime/csp.ts"; -import { withBase } from "../router.ts"; - -export function renderHtml(state: RenderState) { - setRenderState(state); - state.renderingUserTemplate = true; - state.headChildren = false; - - const componentStack = state.componentStack; - try { - const routeComponent = componentStack[componentStack.length - 1]; - let finalComp = h(routeComponent, state.routeOptions) as VNode; - - // Skip page component - let i = componentStack.length - 1; - while (i--) { - const component = componentStack[i] as ComponentType; - const curComp = finalComp; - - finalComp = h(component, { - ...state.routeOptions, - Component() { - return curComp; - }, - // deno-lint-ignore no-explicit-any - } as any) as VNode; - } - - const app = h( - CSP_CONTEXT.Provider, - // deno-lint-ignore no-explicit-any - { value: state.csp } as any, - h(HEAD_CONTEXT.Provider, { - value: state.headVNodes, - children: finalComp, - }), - ) as VNode; - - let html = renderToString(app); - - for (const [id, children] of state.slots.entries()) { - const slotHtml = renderToString(h(Fragment, null, children) as VNode); - const templateId = id.replace(/:/g, "-"); - html += ``; - } - - return html; - } finally { - setRenderState(null); - } -} - -export function renderOuterDocument( - state: RenderState, - opts: { - bodyHtml: string; - lang?: string; - preloads: string[]; - moduleScripts: [src: string, nonce: string][]; - }, -) { - const { - docHtml, - docHead, - renderedHtmlTag, - docBody, - docHeadNodes, - headVNodes, - } = state; - let docTitle = state.docTitle; - - // Filter out duplicate head vnodes by "key" if set - const filteredHeadNodes: VNode[] = []; - - if (headVNodes.length > 0) { - const seen = new Map(); - const userChildren = toChildArray(headVNodes); - for (let i = 0; i < userChildren.length; i++) { - const child = userChildren[i]; - - if (isValidElement(child)) { - if (child.type === "title") { - docTitle = child; - } else if (child.key !== undefined) { - seen.set(child.key, child); - } else { - filteredHeadNodes.push(child); - } - } - } - - if (seen.size > 0) { - filteredHeadNodes.push(...seen.values()); - } - } - - const page = h( - "html", - docHtml ?? { lang: opts.lang }, - h( - "head", - docHead, - !renderedHtmlTag ? h("meta", { charset: "utf-8" }) : null, - !renderedHtmlTag - ? (h("meta", { - name: "viewport", - content: "width=device-width, initial-scale=1.0", - })) - : null, - docTitle, - docHeadNodes.map((node) => h(node.type, node.props)), - opts.preloads.map((src) => - h("link", { rel: "modulepreload", href: withBase(src, state.basePath) }) - ), - opts.moduleScripts.map(([src, nonce]) => - h("script", { - src: withBase(src, state.basePath), - nonce, - type: "module", - }) - ), - filteredHeadNodes, - ), - h("body", { - ...docBody, - dangerouslySetInnerHTML: { __html: opts.bodyHtml }, - }), - ) as VNode; - - try { - setRenderState(state); - return "" + renderToString(page); - } finally { - setRenderState(null); - } -} diff --git a/src/server/router.ts b/src/server/router.ts deleted file mode 100644 index ae0a17a72b1..00000000000 --- a/src/server/router.ts +++ /dev/null @@ -1,261 +0,0 @@ -import { PARTIAL_SEARCH_PARAM } from "../constants.ts"; -import { BaseRoute, FreshContext } from "./types.ts"; - -export type Handler> = ( - req: Request, - ctx: FreshContext, -) => Response | Promise; - -export type FinalHandler = ( - req: Request, - ctx: FreshContext, - route?: InternalRoute, -) => { - destination: DestinationKind; - handler: () => Response | Promise; -}; - -export type ErrorHandler> = ( - req: Request, - ctx: FreshContext, - err: unknown, -) => Response | Promise; - -type UnknownMethodHandler = ( - req: Request, - ctx: FreshContext, - knownMethods: KnownMethod[], -) => Response | Promise; - -export type MatchHandler = ( - req: Request, - ctx: FreshContext, -) => Response | Promise; - -export interface Routes { - [key: string]: { - baseRoute: BaseRoute; - methods: { - [K in KnownMethod | "default"]?: MatchHandler; - }; - }; -} - -export type DestinationKind = "internal" | "static" | "route" | "notFound"; - -export type InternalRoute = { - baseRoute: BaseRoute; - originalPattern: string; - pattern: URLPattern | string; - methods: { [K in KnownMethod]?: MatchHandler }; - default?: MatchHandler; - destination: DestinationKind; -}; - -export interface RouterOptions { - internalRoutes: Routes; - staticRoutes: Routes; - routes: Routes; - otherHandler: Handler; - errorHandler: ErrorHandler; - unknownMethodHandler?: UnknownMethodHandler; -} - -export type KnownMethod = typeof knownMethods[number]; - -export const knownMethods = [ - "GET", - "HEAD", - "POST", - "PUT", - "DELETE", - "OPTIONS", - "PATCH", -] as const; - -export function defaultOtherHandler(_req: Request): Response { - return new Response(null, { - status: 404, - }); -} - -export function defaultErrorHandler( - _req: Request, - ctx: FreshContext, -): Response { - console.error(ctx.error); - - return new Response(null, { - status: 500, - }); -} - -export function defaultUnknownMethodHandler( - _req: Request, - _ctx: FreshContext, - knownMethods: KnownMethod[], -): Response { - return new Response(null, { - status: 405, - headers: { - Accept: knownMethods.join(", "), - }, - }); -} - -export const IS_PATTERN = /[*:{}+?()]/; - -function processRoutes( - processedRoutes: Array, - routes: Routes, - destination: DestinationKind, -) { - for (const [path, def] of Object.entries(routes)) { - const pattern = destination === "static" || !IS_PATTERN.test(path) - ? path - : new URLPattern({ pathname: path }); - - const entry: InternalRoute = { - baseRoute: def.baseRoute, - pattern, - originalPattern: path, - methods: {}, - default: undefined, - destination, - }; - - for (const [method, handler] of Object.entries(def.methods)) { - if (method === "default") { - entry.default = handler; - } else if (knownMethods.includes(method as KnownMethod)) { - entry.methods[method as KnownMethod] = handler; - } - } - - processedRoutes.push(entry); - } -} - -export interface RouteResult { - route: InternalRoute | undefined; - params: Record; - isPartial: boolean; -} - -export function getParamsAndRoute( - { - internalRoutes, - staticRoutes, - routes, - }: RouterOptions, -): ( - url: URL, -) => RouteResult { - const processedRoutes: Array = []; - processRoutes(processedRoutes, internalRoutes, "internal"); - processRoutes(processedRoutes, staticRoutes, "static"); - processRoutes(processedRoutes, routes, "route"); - - const statics = new Map(); - - return (url: URL) => { - const isPartial = url.searchParams.has(PARTIAL_SEARCH_PARAM); - const pathname = url.pathname; - - const cached = statics.get(pathname); - if (cached !== undefined) { - cached.isPartial = isPartial; - return cached; - } - - for (let i = 0; i < processedRoutes.length; i++) { - const route = processedRoutes[i]; - if (route === null) continue; - - // Static routes where the full pattern contains no dynamic - // parts and must be an exact match. We use that for static - // files. - if (typeof route.pattern === "string") { - if (route.pattern === pathname) { - processedRoutes[i] = null; - const res = { route: route, params: {}, isPartial }; - statics.set(route.pattern, res); - return res; - } - - continue; - } - - const res = route.pattern.exec(url); - - if (res !== null) { - return { - route: route, - params: res.pathname.groups, - isPartial, - }; - } - } - return { - route: undefined, - params: {}, - isPartial, - }; - }; -} - -export function router( - { - otherHandler, - unknownMethodHandler, - }: RouterOptions, -): FinalHandler { - unknownMethodHandler ??= defaultUnknownMethodHandler; - - return (req, ctx, route) => { - if (route) { - // If not overridden, HEAD requests should be handled as GET requests but without the body. - if (req.method === "HEAD" && !route.methods["HEAD"]) { - req = new Request(req.url, { method: "GET", headers: req.headers }); - } - - for (const [method, handler] of Object.entries(route.methods)) { - if (req.method === method) { - return { - destination: route.destination, - handler: () => handler(req, ctx), - }; - } - } - - if (route.default) { - return { - destination: route.destination, - handler: () => route.default!(req, ctx), - }; - } else { - return { - destination: route.destination, - handler: () => - unknownMethodHandler!( - req, - ctx, - Object.keys(route.methods) as KnownMethod[], - ), - }; - } - } - - return { - destination: "notFound", - handler: () => otherHandler!(req, ctx), - }; - }; -} - -export function withBase(src: string, base?: string) { - if (base !== undefined && src.startsWith("/") && !src.startsWith(base)) { - return base + src; - } - return src; -} diff --git a/src/server/router_test.ts b/src/server/router_test.ts deleted file mode 100644 index 384ca6567ca..00000000000 --- a/src/server/router_test.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { assertEquals } from "./deps.ts"; -import { IS_PATTERN } from "./router.ts"; - -Deno.test("IS_PATTERN", () => { - assertEquals(IS_PATTERN.test("/foo"), false); - assertEquals(IS_PATTERN.test("/foo/bar/baz.jpg"), false); - assertEquals(IS_PATTERN.test("/foo/:path"), true); - assertEquals(IS_PATTERN.test("/foo/*"), true); - assertEquals(IS_PATTERN.test("/foo{/bar}?"), true); - assertEquals(IS_PATTERN.test("/foo/(\\d+)"), true); - assertEquals(IS_PATTERN.test("/foo/(a)"), true); -}); diff --git a/src/server/serializer.ts b/src/server/serializer.ts deleted file mode 100644 index af3cb46e42a..00000000000 --- a/src/server/serializer.ts +++ /dev/null @@ -1,215 +0,0 @@ -/** - * This module contains a serializer for island props. The serializer is capable - * of serializing the following: - * - * - `null` - * - `boolean` - * - `number` - * - `bigint` - * - `string` - * - `array` - * - `object` (no prototypes) - * - `Uint8Array` - * - `Signal` from `@preact/signals` - * - * Circular references are supported and objects with the same reference are - * serialized only once. - * - * The corresponding deserializer is in `src/runtime/deserializer.ts`. - */ -import { isValidElement, VNode } from "preact"; -import { KEY } from "../runtime/deserializer.ts"; - -interface SerializeResult { - /** The string serialization. */ - serialized: string; - /** If the deserializer is required to deserialize this string. If this is - * `false` the serialized string can be deserialized with `JSON.parse`. */ - requiresDeserializer: boolean; - /** If the serialization contains serialized signals. If this is `true` the - * deserializer must be passed a factory functions for signals. */ - hasSignals: boolean; -} - -interface Signal { - peek(): unknown; - value: unknown; -} - -// deno-lint-ignore no-explicit-any -function isSignal(x: any): x is Signal { - return ( - x !== null && - typeof x === "object" && - typeof x.peek === "function" && - "value" in x - ); -} - -// deno-lint-ignore no-explicit-any -function isVNode(x: any): x is VNode { - return x !== null && typeof x === "object" && "type" in x && "ref" in x && - "__k" in x && - isValidElement(x); -} - -export function serialize(data: unknown): SerializeResult { - let requiresDeserializer = false; - let hasSignals = false; - const seen = new Map(); - const references = new Map<(string | null)[], (string | null)[][]>(); - - const keyStack: (string | null)[] = []; - const parentStack: unknown[] = []; - - let earlyReturn = false; - - const toSerialize = { - v: data, - get r() { - earlyReturn = true; - if (references.size > 0) { - const refs = []; - for (const [targetPath, refPaths] of references) { - refs.push([targetPath, ...refPaths]); - } - return refs; - } - return undefined; - }, - }; - - function replacer( - this: unknown, - key: string | null, - value: unknown, - ): unknown { - if (value === toSerialize || earlyReturn) { - return value; - } - - // Bypass signal's `.toJSON` method because we want to serialize - // the signal itself including the signal's value and not just - // the value. This is needed because `JSON.stringify` always - // calls `.toJSON` automatically if available. - // deno-lint-ignore no-explicit-any - if (key !== null && isSignal((this as any)[key])) { - // deno-lint-ignore no-explicit-any - value = (this as any)[key]; - } - - // For some object types, the path in the object graph from root is not the - // same between the serialized representation, and deserialized objects. For - // these cases, we have to change the contents of the key stack to match the - // deserialized object. - if (typeof this === "object" && this !== null && KEY in this) { - if (this[KEY] === "s" && key === "v") key = "value"; // signals - if (this[KEY] === "l" && key === "v") key = null; // literals (magic key object) - } - - if (this !== toSerialize) { - const parentIndex = parentStack.indexOf(this); - parentStack.splice(parentIndex + 1); - keyStack.splice(parentIndex); - keyStack.push(key); - // the parent is pushed before return - } - - if (typeof value === "object" && value !== null) { - const path = seen.get(value); - const currentPath = [...keyStack]; - if (path !== undefined) { - requiresDeserializer = true; - const referenceArr = references.get(path); - if (referenceArr === undefined) { - references.set(path, [currentPath]); - } else { - referenceArr.push(currentPath); - } - return 0; - } else if (isVNode(value)) { - requiresDeserializer = true; - // No need to serialize JSX as we pick that up from - // the rendered HTML in the browser. - const res = null; - parentStack.push(res); - return res; - } else { - seen.set(value, currentPath); - } - } - - if (isSignal(value)) { - requiresDeserializer = true; - hasSignals = true; - const res = { [KEY]: "s", v: value.peek() }; - parentStack.push(res); - return res; - } else if (typeof value === "bigint") { - requiresDeserializer = true; - const res = { [KEY]: "b", d: value.toString() }; - parentStack.push(res); - return res; - } else if (value instanceof Uint8Array) { - requiresDeserializer = true; - const res = { [KEY]: "u8a", d: b64encode(value) }; - parentStack.push(res); - return res; - } else if (typeof value === "object" && value && KEY in value) { - requiresDeserializer = true; - // deno-lint-ignore no-explicit-any - const v: any = { ...value }; - const k = v[KEY]; - delete v[KEY]; - const res = { [KEY]: "l", k, v }; - parentStack.push(res); - return res; - } else { - parentStack.push(value); - return value; - } - } - - const serialized = JSON.stringify(toSerialize, replacer); - return { serialized, requiresDeserializer, hasSignals }; -} - -// deno-fmt-ignore -const base64abc = [ - "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", - "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "a", "b", "c", "d", - "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", - "t", "u", "v", "w", "x", "y", "z", "0", "1", "2", "3", "4", "5", "6", "7", - "8", "9", "+", "/", -]; - -/** - * CREDIT: https://gist.github.com/enepomnyaschih/72c423f727d395eeaa09697058238727 - * Encodes a given Uint8Array, ArrayBuffer or string into RFC4648 base64 representation - */ -export function b64encode(buffer: ArrayBuffer): string { - const uint8 = new Uint8Array(buffer); - let result = "", - i; - const l = uint8.length; - for (i = 2; i < l; i += 3) { - result += base64abc[uint8[i - 2] >> 2]; - result += base64abc[((uint8[i - 2] & 0x03) << 4) | (uint8[i - 1] >> 4)]; - result += base64abc[((uint8[i - 1] & 0x0f) << 2) | (uint8[i] >> 6)]; - result += base64abc[uint8[i] & 0x3f]; - } - if (i === l + 1) { - // 1 octet yet to write - result += base64abc[uint8[i - 2] >> 2]; - result += base64abc[(uint8[i - 2] & 0x03) << 4]; - result += "=="; - } - if (i === l) { - // 2 octets yet to write - result += base64abc[uint8[i - 2] >> 2]; - result += base64abc[((uint8[i - 2] & 0x03) << 4) | (uint8[i - 1] >> 4)]; - result += base64abc[(uint8[i - 1] & 0x0f) << 2]; - result += "="; - } - return result; -} diff --git a/src/server/serializer_test.ts b/src/server/serializer_test.ts deleted file mode 100644 index 6469e036a96..00000000000 --- a/src/server/serializer_test.ts +++ /dev/null @@ -1,233 +0,0 @@ -// deno-lint-ignore-file no-explicit-any - -import { serialize } from "./serializer.ts"; -import { - assert, - assertEquals, - assertSnapshot, - assertThrows, -} from "../../tests/deps.ts"; -import { deserialize, KEY } from "../runtime/deserializer.ts"; -import { signal } from "@preact/signals-core"; -import { signal as signal130 } from "@preact/signals-core@1.3.0"; -import { signal as signal123 } from "@preact/signals-core@1.2.3"; - -Deno.test("serializer - primitives & plain objects", async (t) => { - const data = { - a: 1, - b: "2", - c: true, - d: null, - f: [1, 2, 3], - g: { a: 1, b: 2, c: 3 }, - }; - const res = serialize(data); - assert(!res.requiresDeserializer); - assert(!res.hasSignals); - await assertSnapshot(t, res.serialized); - const deserialized = deserialize(res.serialized); - assertEquals(deserialized, data); -}); - -Deno.test("serializer - bigint", async (t) => { - const data = { a: 999999999999999999n }; - const res = serialize(data); - assert(res.requiresDeserializer); - assert(!res.hasSignals); - await assertSnapshot(t, res.serialized); - const deserialized = deserialize(res.serialized); - assertEquals(deserialized, data); -}); - -Deno.test("serializer - Uint8Array", async (t) => { - const data = { a: new Uint8Array([1, 2, 3]) }; - const res = serialize(data); - assert(res.requiresDeserializer); - assert(!res.hasSignals); - await assertSnapshot(t, res.serialized); - const deserialized = deserialize(res.serialized); - assertEquals(deserialized, data); -}); - -Deno.test("serializer - signals", async (t) => { - const data = { - a: 1, - b: signal(2), - }; - const res = serialize(data); - assert(res.requiresDeserializer); - assert(res.hasSignals); - await assertSnapshot(t, res.serialized); - const deserialized: any = deserialize(res.serialized, signal); - assertEquals(typeof deserialized, "object"); - assertEquals(deserialized.a, 1); - assertEquals(deserialized.b.value, 2); - assertEquals(deserialized.b.peek(), 2); -}); - -Deno.test("serializer - @preact/signals-core 1.2.3", async (t) => { - const data = { - a: 1, - b: signal123(2), - }; - const res = serialize(data); - assert(res.requiresDeserializer); - assert(res.hasSignals); - await assertSnapshot(t, res.serialized); - const deserialized: any = deserialize(res.serialized, signal); - assertEquals(typeof deserialized, "object"); - assertEquals(deserialized.a, 1); - assertEquals(deserialized.b.value, 2); - assertEquals(deserialized.b.peek(), 2); -}); - -Deno.test("serializer - @preact/signals-core 1.3.0", async (t) => { - const data = { - a: 1, - b: signal130(2), - }; - const res = serialize(data); - assert(res.requiresDeserializer); - assert(res.hasSignals); - await assertSnapshot(t, res.serialized); - const deserialized: any = deserialize(res.serialized, signal); - assertEquals(typeof deserialized, "object"); - assertEquals(deserialized.a, 1); - assertEquals(deserialized.b.value, 2); - assertEquals(deserialized.b.peek(), 2); -}); - -Deno.test("serializer - multiple versions of @preact/signals-core", async (t) => { - const data = { - a: 1, - b: signal(2), - c: signal123(2), - d: signal130(2), - }; - const res = serialize(data); - assert(res.requiresDeserializer); - assert(res.hasSignals); - await assertSnapshot(t, res.serialized); - const deserialized: any = deserialize(res.serialized, signal); - assertEquals(typeof deserialized, "object"); - assertEquals(deserialized.a, 1); - assertEquals(deserialized.b.value, 2); - assertEquals(deserialized.b.peek(), 2); - assertEquals(deserialized.c.value, 2); - assertEquals(deserialized.c.peek(), 2); - assertEquals(deserialized.d.value, 2); - assertEquals(deserialized.d.peek(), 2); -}); - -Deno.test("serializer - magic key", async (t) => { - const data = { [KEY]: "f", a: 1 }; - const res = serialize(data); - assert(res.requiresDeserializer); - assert(!res.hasSignals); - await assertSnapshot(t, res.serialized); - const deserialized = deserialize(res.serialized); - assertEquals(deserialized, data); -}); - -Deno.test("serializer - circular reference objects", async (t) => { - const data: any = { a: 1 }; - data.b = data; - const res = serialize(data); - assert(res.requiresDeserializer); - assert(!res.hasSignals); - await assertSnapshot(t, res.serialized); - const deserialized = deserialize(res.serialized); - assertEquals(deserialized, data); -}); - -Deno.test("serializer - circular reference nested objects", async (t) => { - const data: any = { a: 1, b: { c: 2 } }; - data.b.d = data; - const res = serialize(data); - assert(res.requiresDeserializer); - assert(!res.hasSignals); - await assertSnapshot(t, res.serialized); - const deserialized = deserialize(res.serialized); - assertEquals(deserialized, data); -}); - -Deno.test("serializer - circular reference array", async (t) => { - const data: any = [1, 2, 3]; - data.push(data); - const res = serialize(data); - assert(res.requiresDeserializer); - assert(!res.hasSignals); - await assertSnapshot(t, res.serialized); - const deserialized: any = deserialize(res.serialized); - assertEquals(deserialized, data); - assertEquals(deserialized.length, 4); -}); - -Deno.test("serializer - multiple reference", async (t) => { - const data: any = { a: 1, b: { c: 2 } }; - data.d = data.b; - const res = serialize(data); - assert(res.requiresDeserializer); - assert(!res.hasSignals); - await assertSnapshot(t, res.serialized); - const deserialized = deserialize(res.serialized); - assertEquals(deserialized, data); -}); - -Deno.test("serializer - multiple reference signals", async (t) => { - const inner: any = { [KEY]: "x", x: 1 }; - inner.y = inner; - const s = signal(inner); - const data = { inner, a: s, b: { c: s } }; - const res = serialize(data); - assert(res.requiresDeserializer); - assert(res.hasSignals); - await assertSnapshot(t, res.serialized); - const deserialized: any = deserialize(res.serialized, signal); - assertEquals(deserialized.a.value, inner); - assertEquals(deserialized.a.peek(), inner); - assertEquals(deserialized.b.c.value, inner); - assertEquals(deserialized.b.c.peek(), inner); - deserialized.a.value = 2; - assertEquals(deserialized.a.value, 2); - assertEquals(deserialized.b.c.value, 2); -}); - -Deno.test("serializer - multiple reference in magic key", async (t) => { - const inner = { foo: "bar" }; - const literal: any = { [KEY]: "x", inner }; - const data = { literal, inner }; - const res = serialize(data); - assert(res.requiresDeserializer); - assert(!res.hasSignals); - await assertSnapshot(t, res.serialized); - const deserialized: any = deserialize(res.serialized); - assertEquals(deserialized, data); -}); - -Deno.test("serializer - multiple reference in signal", async (t) => { - const inner = { foo: "bar" }; - const s = signal(inner); - const data = { s, inner }; - const res = serialize(data); - assert(res.requiresDeserializer); - assert(res.hasSignals); - await assertSnapshot(t, res.serialized); - const deserialized: any = deserialize(res.serialized, signal); - assertEquals(deserialized.s.value, inner); - assertEquals(deserialized.s.peek(), inner); - assertEquals(deserialized.inner, inner); -}); - -Deno.test("deserializer - no prototype pollution with manual input", () => { - const serialized = String.raw`{ - "v": { - "*": ["onerror"] - }, - "r": [ - [["*"], ["constructor", "prototype", "*"]] - ] - }`; - assertThrows(() => deserialize(serialized, signal)); - assertEquals({}.constructor.prototype["*"], undefined); -}); diff --git a/src/server/types.ts b/src/server/types.ts deleted file mode 100644 index d3fce28b1ce..00000000000 --- a/src/server/types.ts +++ /dev/null @@ -1,675 +0,0 @@ -import { ComponentChildren, ComponentType, VNode } from "preact"; -import * as router from "./router.ts"; -import { InnerRenderFunction, RenderContext } from "./render.ts"; -import { Manifest } from "./mod.ts"; - -export interface DenoConfig { - imports?: Record; - importMap?: string; - tasks?: Record; - lint?: { - rules: { tags?: string[] }; - exclude?: string[]; - }; - fmt?: { - exclude?: string[]; - }; - exclude?: string[]; - compilerOptions?: { - jsx?: string; - jsxImportSource?: string; - }; -} - -// --- APPLICATION CONFIGURATION --- -export interface FreshConfig { - build?: { - /** - * The directory to write generated files to when `dev.ts build` is run. - * This can be an absolute path, a file URL or a relative path. - */ - outDir?: string; - /** - * This sets the target environment for the generated code. Newer - * language constructs will be transformed to match the specified - * support range. See https://esbuild.github.io/api/#target - * @default {"es2022"} - */ - target?: string | string[]; - }; - render?: RenderFunction; - plugins?: Plugin[]; - staticDir?: string; - router?: RouterOptions; - server?: Partial; - - // Older versions of Fresh merged the `Deno.ServeTlsOptions` directly. - // We've moved this to `server`. - - /** - * Server private key in PEM format - * @deprecated Use `server.cert` instead - */ - cert?: string; - /** - * Cert chain in PEM format - * @deprecated Use `server.key` instead - */ - key?: string; - /** - * The port to listen on. - * @default {8000} - * @deprecated Use `server.port` instead - */ - port?: number; - /** - * A literal IP address or host name that can be resolved to an IP address. - * - * __Note about `0.0.0.0`__ While listening `0.0.0.0` works on all platforms, - * the browsers on Windows don't work with the address `0.0.0.0`. - * You should show the message like `server running on localhost:8080` instead of - * `server running on 0.0.0.0:8080` if your program supports Windows. - * - * @default {"0.0.0.0"} - * @deprecated Use `server.hostname` instead - */ - hostname?: string; - /** - * An {@linkcode AbortSignal} to close the server and all connections. - * @deprecated Use `server.signal` instead - */ - signal?: AbortSignal; - /** - * Sets `SO_REUSEPORT` on POSIX systems. - * @deprecated Use `server.reusePort` instead - */ - reusePort?: boolean; - /** - * The handler to invoke when route handlers throw an error. - * @deprecated Use `server.onError` instead - */ - onError?: (error: unknown) => Response | Promise; - - /** - * The callback which is called when the server starts listening. - * @deprecated Use `server.onListen` instead - */ - onListen?: (params: { hostname: string; port: number }) => void; -} - -export interface InternalFreshState { - config: ResolvedFreshConfig; - manifest: Manifest; - loadSnapshot: boolean; - didLoadSnapshot: boolean; - denoJsonPath: string; - denoJson: DenoConfig; - build: boolean; -} - -export interface ResolvedFreshConfig { - dev: boolean; - build: { - outDir: string; - target: string | string[]; - }; - render: RenderFunction; - plugins: Plugin[]; - staticDir: string; - router?: RouterOptions; - server: Partial; - basePath: string; -} - -export interface RouterOptions { - /** - * Controls whether Fresh will append a trailing slash to the URL. - * @default {false} - */ - trailingSlash?: boolean; - /** - * Configures the pattern of files to ignore in islands and routes. - * - * By default Fresh will ignore test files, - * for example files with a `.test.ts` or a `_test.ts` suffix. - * - * @default {/(?:[^/]*_|[^/]*\.|)test\.(?:ts|tsx|mts|js|mjs|jsx|)\/*$/} - */ - ignoreFilePattern?: RegExp; - /** - * Serve fresh from a base path instead of from the root. - * "/foo/bar" -> http://localhost:8000/foo/bar - * @default {undefined} - */ - basePath?: string; -} - -export type RenderFunction = ( - ctx: RenderContext, - render: InnerRenderFunction, -) => void | Promise; - -/// --- ROUTES --- - -// deno-lint-ignore no-explicit-any -export type PageProps> = Omit< - FreshContext< - S, - T - >, - "render" | "next" | "renderNotFound" ->; - -export interface StaticFile { - /** The URL to the static file on disk. */ - localUrl: URL; - /** The path to the file as it would be in the incoming request. */ - path: string; - /** The size of the file. */ - size: number; - /** The content-type of the file. */ - contentType: string; - /** Hash of the file contents. */ - etag: string; -} - -export interface FreshContext< - State = Record, - // deno-lint-ignore no-explicit-any - Data = any, - NotFoundData = Data, -> { - /** @types deprecated */ - localAddr?: Deno.NetAddr; - remoteAddr: Deno.NetAddr; - url: URL; - basePath: string; - route: string; - /** @deprecated Use `.route` instead */ - pattern: string; - destination: router.DestinationKind; - params: Record; - isPartial: boolean; - state: State; - config: ResolvedFreshConfig; - data: Data; - /** The error that caused the error page to be loaded. */ - error?: unknown; - /** Sringified code frame of the error rendering failed (only in development mode) */ - codeFrame?: unknown; - - // These properties may be different - renderNotFound: (data?: NotFoundData) => Response | Promise; - render: ( - data?: Data, - options?: RenderOptions, - ) => Response | Promise; - Component: ComponentType; - next: () => Promise; -} -/** - * Context passed to async route components. - */ -// deno-lint-ignore no-explicit-any -export type RouteContext> = Omit< - FreshContext< - S, - T - >, - "next" | "render" ->; - -export interface RouteConfig { - /** - * A route override for the page. This is useful for pages where the route - * can not be expressed through the filesystem routing capabilities. - * - * The route override must be a path-to-regexp compatible route matcher. - */ - routeOverride?: string; - - /** - * If Content-Security-Policy should be enabled for this page. If 'true', a - * locked down policy will be used that allows only the scripts and styles - * that are generated by Fresh. Additional scripts and styles can be added - * using the `useCSP` hook. - */ - csp?: boolean; - - /** - * Skip already inherited layouts - * Default: `false` - */ - skipInheritedLayouts?: boolean; - - /** - * Skip rendering the `routes/_app` template - * Default: `false` - */ - skipAppWrapper?: boolean; -} - -export interface RenderOptions extends ResponseInit {} - -export type ServeHandlerInfo = Deno.ServeHandlerInfo & { - /** - * Backwards compatible with std/server - * @deprecated - */ - localAddr?: Deno.NetAddr; -}; - -export type ServeHandler = ( - request: Request, - info: ServeHandlerInfo, -) => Response | Promise; - -// deno-lint-ignore no-explicit-any -export type Handler> = ( - req: Request, - ctx: FreshContext, -) => Response | Promise; - -// deno-lint-ignore no-explicit-any -export type Handlers> = { - [K in router.KnownMethod]?: Handler; -}; - -export interface RouteModule { - default?: PageComponent; - // deno-lint-ignore no-explicit-any - handler?: Handler | Handlers; - config?: RouteConfig; -} - -// deno-lint-ignore no-explicit-any -export type AsyncRoute> = ( - req: Request, - ctx: FreshContext, -) => Promise; -// deno-lint-ignore no-explicit-any -export type PageComponent> = - | ComponentType> - | AsyncRoute - // deno-lint-ignore no-explicit-any - | ((props: any) => VNode | ComponentChildren); - -// deno-lint-ignore no-explicit-any -export interface Route { - baseRoute: BaseRoute; - pattern: string; - url: string; - name: string; - component?: PageComponent | AsyncLayout | AsyncRoute; - handler: Handler | Handlers; - csp: boolean; - appWrapper: boolean; - inheritLayouts: boolean; -} - -// --- APP --- -export interface AppModule { - default: ComponentType | AsyncLayout; -} - -// deno-lint-ignore no-explicit-any -export type AsyncLayout> = ( - req: Request, - ctx: FreshContext, -) => Promise; - -export interface LayoutConfig { - /** - * Skip already inherited layouts - * Default: `false` - */ - skipAppWrapper?: boolean; - /** - * Skip rendering the `routes/_app`. - * Default: `false` - */ - skipInheritedLayouts?: boolean; -} - -export interface LayoutModule { - // deno-lint-ignore no-explicit-any - handler?: Handler | Handlers; - default: ComponentType | AsyncLayout; - config?: LayoutConfig; -} - -export interface LayoutRoute { - baseRoute: BaseRoute; - // deno-lint-ignore no-explicit-any - handler?: Handler | Handlers; - component: LayoutModule["default"]; - appWrapper: boolean; - inheritLayouts: boolean; -} - -// --- UNKNOWN PAGE --- - -export type UnknownHandler = ( - req: Request, - ctx: FreshContext, -) => Response | Promise; - -export interface UnknownPageModule { - default?: PageComponent; - handler?: UnknownHandler; - config?: RouteConfig; -} - -export interface UnknownPage { - baseRoute: BaseRoute; - pattern: string; - url: string; - name: string; - component?: PageComponent; - handler: UnknownHandler; - csp: boolean; - appWrapper: boolean; - inheritLayouts: boolean; -} - -export type UnknownRenderFunction = ( - req: Request, - ctx: FreshContext, -) => Promise; - -// --- ERROR PAGE --- - -// Nominal/Branded type. Ensures that the string has the expected format -export type BaseRoute = string & { readonly __brand: unique symbol }; - -export type ErrorHandler = ( - req: Request, - ctx: FreshContext, -) => Response | Promise; - -export interface ErrorPageModule { - default?: PageComponent; - handler?: ErrorHandler; - config?: RouteConfig; -} - -export interface ErrorPage { - baseRoute: BaseRoute; - pattern: string; - url: string; - name: string; - component?: PageComponent; - handler: ErrorHandler; - csp: boolean; - appWrapper: boolean; - inheritLayouts: boolean; -} - -// --- MIDDLEWARES --- -export interface MiddlewareRoute { - baseRoute: BaseRoute; - module: Middleware; -} - -export type MiddlewareHandler> = ( - req: Request, - ctx: FreshContext, -) => Response | Promise; - -// deno-lint-ignore no-explicit-any -export interface MiddlewareModule { - handler: MiddlewareHandler | MiddlewareHandler[]; -} - -export interface Middleware> { - handler: MiddlewareHandler | MiddlewareHandler[]; -} - -// --- ISLANDS --- - -export interface IslandModule { - // deno-lint-ignore no-explicit-any - [key: string]: ComponentType | unknown; -} - -export interface Island { - id: string; - name: string; - url: string; - component: ComponentType; - exportName: string; -} - -// --- PLUGINS --- - -export interface Plugin> { - /** The name of the plugin. Must be snake-case. */ - name: string; - - /** A map of a snake-case names to a import specifiers. The entrypoints - * declared here can later be used in the "scripts" option of - * `PluginRenderResult` to load the entrypoint's code on the client. - */ - entrypoints?: Record; - - /** The render hook is called on the server every time some JSX needs to - * be turned into HTML. The render hook needs to call the `ctx.render` - * function exactly once. - * - * The hook can return a `PluginRenderResult` object that can do things like - * inject CSS into the page, or load additional JS files on the client. - */ - render?(ctx: PluginRenderContext): PluginRenderResult; - - /** The asynchronous render hook is called on the server every time some - * JSX needs to be turned into HTML, wrapped around all synchronous render - * hooks. The render hook needs to call the `ctx.renderAsync` function - * exactly once, and await the result. - * - * This is useful for when plugins are generating styles and scripts with - * asynchronous dependencies. Unlike the synchronous render hook, async render - * hooks for multiple pages can be running at the same time. This means that - * unlike the synchronous render hook, you can not use global variables to - * propagate state between the render hook and the renderer. - */ - renderAsync?(ctx: PluginAsyncRenderContext): Promise; - - /** - * Called before running the Fresh build task - */ - buildStart?(config: ResolvedFreshConfig): Promise | void; - /** - * Called after completing the Fresh build task - */ - buildEnd?(): Promise | void; - - /** - * Called after configuration has been loaded - */ - configResolved?(config: ResolvedFreshConfig): Promise | void; - - routes?: PluginRoute[]; - - middlewares?: PluginMiddleware[]; - - islands?: PluginIslands; -} - -export interface PluginRenderContext { - render: PluginRenderFunction; -} - -export interface PluginAsyncRenderContext { - renderAsync: PluginAsyncRenderFunction; -} - -export interface PluginRenderResult { - /** CSS styles to be injected into the page. */ - styles?: PluginRenderStyleTag[]; - /** JS scripts to ship to the client. */ - scripts?: PluginRenderScripts[]; - /** Link tags for the page */ - links?: PluginRenderLink[]; - /** Body HTML transformed by the plugin */ - htmlText?: string; -} - -export interface PluginRenderStyleTag { - cssText: string; - media?: string; - id?: string; -} - -export type PluginRenderLink = { - crossOrigin?: string; - href?: string; - hreflang?: string; - media?: string; - referrerPolicy?: string; - rel?: string; - title?: string; - type?: string; -}; - -export interface PluginRenderScripts { - /** The "key" of the entrypoint (as specified in `Plugin.entrypoints`) for the - * script that should be loaded. The script must be an ES module that exports - * a default function. - * - * The default function is invoked with the `state` argument specified below. - */ - entrypoint: string; - /** The state argument that is passed to the default export invocation of the - * entrypoint's default export. The state must be JSON-serializable. - */ - state: unknown; -} - -export type PluginRenderFunction = () => PluginRenderFunctionResult; - -export type PluginAsyncRenderFunction = () => - | PluginRenderFunctionResult - | Promise; - -export interface PluginRenderFunctionResult { - /** The HTML text that was rendered. */ - htmlText: string; - /** If the renderer encountered any islands that require hydration on the - * client. */ - requiresHydration: boolean; -} - -export interface PluginMiddleware> { - /** A path in the format of a filename path without filetype */ - path: string; - - middleware: Middleware; -} - -export interface PluginRoute { - /** A path in the format of a filename path without filetype */ - path: string; - - component?: - | ComponentType - | ComponentType - | AsyncRoute - | AsyncLayout; - - // deno-lint-ignore no-explicit-any - handler?: Handler | Handlers; -} - -export interface PluginIslands { - baseLocation: string; - paths: string[]; -} - -// --- Deprecated types --- - -/** - * @deprecated This type was a short-lived alternative to `Handlers`. Please use `Handlers` instead. - */ -export type MultiHandler = Handlers; - -/** - * @deprecated Use {@linkcode FreshConfig} instead - */ -export type StartOptions = FreshConfig; - -/** - * @deprecated Use {@linkcode FreshConfig} interface instead. - */ -export type FreshOptions = FreshConfig; - -/** - * @deprecated Use {@linkcode FreshContext} interface instead - */ -export type HandlerContext< - Data = unknown, - State = Record, - NotFoundData = Data, -> = FreshContext; - -/** - * @deprecated Use {@linkcode FreshContext} interface instead - */ -// deno-lint-ignore no-explicit-any -export type AppContext> = FreshContext< - S, - T ->; - -/** - * @deprecated Use {@linkcode FreshContext} interface instead - */ -// deno-lint-ignore no-explicit-any -export type LayoutContext> = FreshContext< - S, - T ->; - -/** - * @deprecated Use {@linkcode FreshContext} interface instead - */ -export type UnknownHandlerContext> = - FreshContext; - -/** - * @deprecated Use {@linkcode FreshContext} interface instead - */ -export type ErrorHandlerContext> = FreshContext< - State ->; - -/** - * @deprecated Use {@linkcode FreshContext} interface instead - */ -export type MiddlewareHandlerContext> = - FreshContext; - -/** - * @deprecated Use {@linkcode PageProps} instead - */ -// deno-lint-ignore no-explicit-any -export type LayoutProps> = PageProps; - -/** - * @deprecated Use {@linkcode PageProps} instead - */ -// deno-lint-ignore no-explicit-any -export type UnknownPageProps> = PageProps< - T, - S ->; - -/** - * @deprecated Use {@linkcode PageProps} instead - */ -// deno-lint-ignore no-explicit-any -export type AppProps> = PageProps; - -/** - * @deprecated Use {@linkcode PageProps} instead - */ -export type ErrorPageProps = PageProps; diff --git a/src/test_utils.ts b/src/test_utils.ts new file mode 100644 index 00000000000..7291b64fc8f --- /dev/null +++ b/src/test_utils.ts @@ -0,0 +1,112 @@ +import { FreshReqContext } from "./context.ts"; +import type { MiddlewareFn } from "./middlewares/mod.ts"; +import type { FsAdapter } from "./fs.ts"; +import { type BuildCache, ProdBuildCache } from "./build_cache.ts"; +import type { ResolvedFreshConfig } from "./config.ts"; +import type { WalkEntry } from "@std/fs/walk"; +import { DEFAULT_CONN_INFO } from "./app.ts"; + +const STUB = {} as unknown as Deno.ServeHandlerInfo; + +export class FakeServer { + constructor( + public handler: ( + req: Request, + info: Deno.ServeHandlerInfo, + ) => Response | Promise, + ) {} + + async get(path: string, init?: RequestInit): Promise { + const url = this.toUrl(path); + const req = new Request(url, init); + return await this.handler(req, STUB); + } + async post(path: string, body?: BodyInit): Promise { + const url = this.toUrl(path); + const req = new Request(url, { method: "post", body }); + return await this.handler(req, STUB); + } + async patch(path: string, body?: BodyInit): Promise { + const url = this.toUrl(path); + const req = new Request(url, { method: "patch", body }); + return await this.handler(req, STUB); + } + async put(path: string, body?: BodyInit): Promise { + const url = this.toUrl(path); + const req = new Request(url, { method: "put", body }); + return await this.handler(req, STUB); + } + async delete(path: string): Promise { + const url = this.toUrl(path); + const req = new Request(url, { method: "delete" }); + return await this.handler(req, STUB); + } + async head(path: string): Promise { + const url = this.toUrl(path); + const req = new Request(url, { method: "head" }); + return await this.handler(req, STUB); + } + + private toUrl(path: string) { + return new URL(path, "http://localhost/"); + } +} + +const DEFAULT_CONFIG: ResolvedFreshConfig = { + build: { + outDir: "", + }, + mode: "production", + basePath: "", + root: "", + staticDir: "", +}; + +export function serveMiddleware( + middleware: MiddlewareFn, + options: { config?: ResolvedFreshConfig; buildCache?: BuildCache } = {}, +): FakeServer { + return new FakeServer(async (req) => { + const next = () => new Response("not found", { status: 404 }); + const config = options.config ?? DEFAULT_CONFIG; + const buildCache = options.buildCache ?? + new ProdBuildCache(config, new Map(), new Map(), true); + + const ctx = new FreshReqContext( + req, + config, + () => Promise.resolve(next()), + new Map(), + buildCache, + DEFAULT_CONN_INFO, + ); + return await middleware(ctx); + }); +} + +export function createFakeFs(files: Record): FsAdapter { + return { + async *walk(_root) { + // FIXME: ignore + for (const file of Object.keys(files)) { + const entry: WalkEntry = { + isDirectory: false, + isFile: true, + isSymlink: false, + name: file, // FIXME? + path: file, + }; + yield entry; + } + }, + // deno-lint-ignore require-await + async isDirectory(dir) { + return Object.keys(files).some((file) => file.startsWith(dir + "/")); + }, + async mkdirp(_dir: string) { + }, + readFile: Deno.readFile, + }; +} + +export const delay = (ms: number) => new Promise((r) => setTimeout(r, ms)); diff --git a/src/types.ts b/src/types.ts index fc03cfc9ae1..c7d6ea3429c 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,21 +1,52 @@ +export interface RouteConfig { + /** + * A route override for the page. This is useful for pages where the route + * can not be expressed through the filesystem routing capabilities. + * + * The route override must be a path-to-regexp compatible route matcher. + */ + routeOverride?: string; + + /** + * If Content-Security-Policy should be enabled for this page. If 'true', a + * locked down policy will be used that allows only the scripts and styles + * that are generated by Fresh. Additional scripts and styles can be added + * using the `useCSP` hook. + */ + csp?: boolean; + + /** + * Skip already inherited layouts + * Default: `false` + */ + skipInheritedLayouts?: boolean; + + /** + * Skip rendering the `routes/_app` template + * Default: `false` + */ + skipAppWrapper?: boolean; +} + export interface SignalLike { value: T; peek(): T; subscribe(fn: (value: T) => void): () => void; } -declare global { - namespace preact.createElement.JSX { - interface HTMLAttributes { - /** - * Alternative url to fetch partials from on `` or `
` tags - */ - "f-partial"?: string | SignalLike; - /** - * Enable or disable client side navigation and partials for this - * particular node and its children. - */ - "f-client-nav"?: boolean | SignalLike; - } - } -} +// TODO: Uncomment once JSR supports global types +// declare global { +// namespace preact.createElement.JSX { +// interface HTMLAttributes { +// /** +// * Alternative url to fetch partials from on `` or `` tags +// */ +// "f-partial"?: string | SignalLike; +// /** +// * Enable or disable client side navigation and partials for this +// * particular node and its children. +// */ +// "f-client-nav"?: boolean | SignalLike; +// } +// } +// } diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 00000000000..139de868fce --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,39 @@ +import * as path from "@std/path"; + +const PERIODS = { + year: 365 * 24 * 60 * 60 * 1000, + month: 30 * 24 * 60 * 60 * 1000, + week: 7 * 24 * 60 * 60 * 1000, + day: 24 * 60 * 60 * 1000, + hour: 60 * 60 * 1000, + minute: 60 * 1000, + seconds: 1000, +}; + +export function prettyTime(diff: number) { + if (diff > PERIODS.day) { + return Math.floor(diff / PERIODS.day) + "d"; + } else if (diff > PERIODS.hour) { + return Math.floor(diff / PERIODS.hour) + "h"; + } else if (diff > PERIODS.minute) { + return Math.floor(diff / PERIODS.minute) + "m"; + } else if (diff > PERIODS.seconds) { + return Math.floor(diff / PERIODS.seconds) + "s"; + } + + return diff + "ms"; +} + +export function assertInDir( + filePath: string, + dir: string, +): void { + let tmp = filePath; + if (!path.isAbsolute(tmp)) { + tmp = path.join(dir, filePath); + } + + if (path.relative(dir, tmp).startsWith(".")) { + throw new Error(`Path "${tmp}" resolved outside of "${dir}"`); + } +} diff --git a/tests/active_links_test.tsx b/tests/active_links_test.tsx new file mode 100644 index 00000000000..ad152a0ba21 --- /dev/null +++ b/tests/active_links_test.tsx @@ -0,0 +1,176 @@ +import { App, staticFiles } from "@fresh/core"; +import { + allIslandApp, + assertNotSelector, + assertSelector, + buildProd, + Doc, + getIsland, + parseHtml, + withBrowserApp, +} from "./test_utils.tsx"; + +import { SelfCounter } from "./fixtures_islands/SelfCounter.tsx"; +import { PartialInIsland } from "./fixtures_islands/PartialInIsland.tsx"; +import { JsonIsland } from "./fixtures_islands/JsonIsland.tsx"; +import { FakeServer } from "../src/test_utils.ts"; +import { Partial } from "@fresh/core/runtime"; +import { getBuildCache, setBuildCache } from "../src/app.ts"; + +await buildProd(allIslandApp); + +function testApp(): App { + const selfCounter = getIsland("SelfCounter.tsx"); + const partialInIsland = getIsland("PartialInIsland.tsx"); + const jsonIsland = getIsland("JsonIsland.tsx"); + + const app = new App() + .island(selfCounter, "SelfCounter", SelfCounter) + .island(partialInIsland, "PartialInIsland", PartialInIsland) + .island(jsonIsland, "JsonIsland", JsonIsland) + .use(staticFiles()); + setBuildCache(app, getBuildCache(allIslandApp)); + return app; +} + +Deno.test("active links - without client nav", async () => { + function View() { + return ( + + + + ); + } + + const app = testApp() + .get("/active_nav/foo", (ctx) => { + return ctx.render(); + }) + .get("/active_nav", (ctx) => { + return ctx.render(); + }); + + const server = new FakeServer(await app.handler()); + let res = await server.get("/active_nav"); + let doc = parseHtml(await res.text()); + + assertSelector(doc, "a[href='/'][data-ancestor]"); + + // Current + assertNotSelector(doc, "a[href='/active_nav'][data-ancestor]"); + assertSelector(doc, "a[href='/active_nav'][data-current]"); + assertSelector(doc, `a[href='/active_nav'][aria-current="page"]`); + + // Unrelated links + assertNotSelector(doc, "a[href='/active_nav/foo'][data-ancestor]"); + assertNotSelector(doc, "a[href='/active_nav/foo'][aria-current]"); + assertNotSelector(doc, "a[href='/active_nav/foo/bar'][data-ancestor]"); + assertNotSelector(doc, "a[href='/active_nav/foo/bar'][aria-current]"); + + res = await server.get(`/active_nav/foo`); + doc = parseHtml(await res.text()); + assertSelector(doc, "a[href='/active_nav/foo'][data-current]"); + assertSelector(doc, `a[href='/active_nav/foo'][aria-current="page"]`); + assertSelector(doc, "a[href='/active_nav'][data-ancestor]"); + assertSelector(doc, `a[href='/active_nav'][aria-current="true"]`); + assertSelector(doc, "a[href='/'][data-ancestor]"); + assertSelector(doc, `a[href='/'][aria-current="true"]`); +}); + +Deno.test({ + name: "active links - updates outside of vdom", + fn: async () => { + function PartialPage() { + return ( +
+ +

/active_nav_partial

+
+

+ + /active_nav_partial/foo/bar + +

+

+ /active_nav_partial/foo +

+

+ /active_nav_partial +

+

+ / +

+
+ ); + } + + const app = testApp() + .get("/active_nav_partial/foo", (ctx) => { + return ctx.render(); + }) + .get("/active_nav_partial", (ctx) => { + return ctx.render(); + }); + + await withBrowserApp(app, async (page, address) => { + await page.goto(`${address}/active_nav_partial`); + + let doc = parseHtml(await page.content()); + assertSelector(doc, "a[href='/'][data-ancestor]"); + + // Current + assertNotSelector(doc, "a[href='/active_nav_partial'][data-ancestor]"); + assertSelector(doc, "a[href='/active_nav_partial'][data-current]"); + assertSelector(doc, `a[href='/active_nav_partial'][aria-current="page"]`); + + // Unrelated links + assertNotSelector( + doc, + "a[href='/active_nav_partial/foo'][data-ancestor]", + ); + assertNotSelector( + doc, + "a[href='/active_nav_partial/foo'][aria-current]", + ); + assertNotSelector( + doc, + "a[href='/active_nav_partial/foo/bar'][data-ancestor]", + ); + assertNotSelector( + doc, + "a[href='/active_nav_partial/foo/bar'][aria-current]", + ); + + await page.goto(`${address}/active_nav_partial/foo`); + doc = parseHtml(await page.content()); + assertSelector(doc, "a[href='/active_nav_partial/foo'][data-current]"); + assertSelector( + doc, + `a[href='/active_nav_partial/foo'][aria-current="page"]`, + ); + assertSelector(doc, "a[href='/active_nav_partial'][data-ancestor]"); + assertSelector( + doc, + `a[href='/active_nav_partial'][data-ancestor][aria-current="true"]`, + ); + assertSelector(doc, "a[href='/'][data-ancestor]"); + assertSelector(doc, `a[href='/'][aria-current="true"]`); + }); + }, + sanitizeResources: false, + sanitizeOps: false, +}); diff --git a/tests/base_path_test.ts b/tests/base_path_test.ts deleted file mode 100644 index 76435c1496b..00000000000 --- a/tests/base_path_test.ts +++ /dev/null @@ -1,350 +0,0 @@ -import { - assert, - assertEquals, - assertMatch, - assertStringIncludes, - delay, - Page, - Project, - puppeteer, - STATUS_CODE, -} from "./deps.ts"; -import { - clickWhenListenerReady, - getErrorOverlay, - runBuild, - waitForText, - withFakeServe, - withFresh, - withPageName, -} from "./test_utils.ts"; - -Deno.test("redirects on incomplete base path in url", async () => { - await withFresh("./tests/fixture_base_path/main.ts", async (address) => { - await delay(100); - - const url = new URL(address); - const res = await fetch(url.origin); - assert( - res.url.endsWith("/foo/bar"), - `didn't redirect to base path: "${res.url}"`, - ); - assert(res.redirected, "did not redirect"); - assertEquals(res.status, STATUS_CODE.OK); - await res.body?.cancel(); - }); -}); - -Deno.test("shows full address with base path in cli", async () => { - // deno-lint-ignore require-await - await withFresh("./tests/fixture_base_path/main.ts", async (address) => { - assertMatch(address, /^http:\/\/localhost:\d+\/foo\/bar$/); - }); -}); - -Deno.test("rewrites middleware request", async () => { - await withFresh("./tests/fixture_base_path/main.ts", async (address) => { - const res = await fetch(`${address}/api`); - const body = await res.text(); - assertEquals(body, "middleware is working"); - }); -}); - -Deno.test("rewrites root relative middleware redirects", async () => { - await withFresh("./tests/fixture_base_path/main.ts", async (address) => { - const res = await fetch(`${address}/api/rewrite`); - assertEquals(res.status, STATUS_CODE.OK); - assertEquals(res.url, address); - await res.body?.cancel(); - }); -}); - -Deno.test("passes basePath to route handlers", async () => { - await withFresh("./tests/fixture_base_path/main.ts", async (address) => { - const res = await fetch(`${address}/api/base-handler`); - assertEquals(res.status, STATUS_CODE.OK); - assertEquals(await res.text(), "/foo/bar"); - }); -}); - -Deno.test("works with relative urls", async () => { - await withFresh("./tests/fixture_base_path/main.ts", async (address) => { - const browser = await puppeteer.launch({ args: ["--no-sandbox"] }); - const page = await browser.newPage(); - await page.goto(`${address}/a/b/relative`); - - await Promise.all([ - page.waitForNavigation(), - page.click("a"), - ]); - - const html = await page.content(); - assertMatch(html, /it works/); - await browser.close(); - }); -}); - -Deno.test("rewrites root relative URLs in HTML", async () => { - await withFresh("./tests/fixture_base_path/main.ts", async (address) => { - const browser = await puppeteer.launch({ args: ["--no-sandbox"] }); - const page = await browser.newPage(); - await page.goto(`${address}/html`); - - const script = await page.$eval("#script-output", (el) => el.textContent); - assertEquals(script, "it works"); - - const img = await page.$eval("img", (el) => ({ - src: el.src, - })); - assertMatch(img.src, /\/foo\/bar\/img\.png/); - - const img2 = await page.$eval(".img-srcset", (el) => ({ - src: el.src, - srcset: el.srcset, - })); - assertMatch(img2.src, /\/foo\/bar\/img\.png/); - assertMatch( - img2.srcset, - /\/foo\/bar\/img\.png.* 480w,.*\/foo\/bar\/img\.png.* 800w/, - ); - - const source = await page.$eval("picture source", (el) => ({ - srcset: el.srcset, - })); - assertMatch( - source.srcset, - /\/foo\/bar\/img\.png/, - ); - - const style = await page.$eval( - ".foo", - (el) => globalThis.getComputedStyle(el).color, - ); - assertMatch( - style, - /rgb\(255,\s+0,\s+0\)/, - ); - - await browser.close(); - }); -}); - -Deno.test("island tests", async (t) => { - await withPageName( - "./tests/fixture_base_path/main.ts", - async (page, address) => { - async function counterTest(counterId: string, originalValue: number) { - const pElem = await page.waitForSelector(`#${counterId} > p`); - - const value = await pElem?.evaluate((el) => el.textContent); - assert(value === `${originalValue}`, `${counterId} first value`); - - await clickWhenListenerReady(page, `#b-${counterId}`); - await waitForText(page, `#${counterId} > p`, String(originalValue + 1)); - } - - await page.goto(`${address}/islands`); - - await t.step("Ensure 1 islands on 1 page are revived", async () => { - await counterTest("counter1", 3); - }); - }, - ); -}); - -Deno.test("assets prefixed dev", async (t) => { - await withPageName( - "./tests/fixture_base_path/dev.ts", - async (page, address) => { - await page.goto(`${address}/islands`); - - await t.step("ensure every preload link is prefixed", async () => { - await checkPreloadLinks(page, "/foo/bar"); - }); - - await t.step("ensure every script link is prefixed", async () => { - await checkScriptSrcs(page, "/foo/bar"); - }); - - await t.step("ensure inline content is prefixed", async () => { - await checkInlineScripts(page, "/foo/bar"); - }); - }, - ); -}); - -Deno.test("assets prefixed main", async (t) => { - await withPageName( - "./tests/fixture_base_path/main.ts", - async (page, address) => { - await page.goto(`${address}/islands`); - - await t.step("ensure every preload link is prefixed", async () => { - await checkPreloadLinks(page, "/foo/bar"); - }); - - // no script sent out, because dev sends out dev_client.js - - await t.step("ensure inline content is prefixed", async () => { - await checkInlineScripts(page, "/foo/bar"); - }); - }, - ); -}); - -Deno.test("renders error boundary", async () => { - await withPageName( - "./tests/fixture_base_path/main.ts", - async (page, address) => { - await page.goto(`${address}/error_boundary`); - const text = await page.$eval("p", (el) => el.textContent); - assertEquals(text, "it works"); - }, - ); -}); - -Deno.test("dev_command config: shows codeframe", async () => { - await withFakeServe( - "./tests/fixture_base_path/dev.ts", - async (server) => { - const { codeFrame } = await getErrorOverlay(server, "/codeframe"); - assert(codeFrame); - }, - ); -}); - -Deno.test("TailwindCSS - dev mode", async () => { - await withFakeServe("./tests/fixture_base_path/dev.ts", async (server) => { - const res = await server.get("/styles.css"); - const content = await res.text(); - assertStringIncludes(content, ".text-red-600"); - - const res2 = await server.get("/styles.css?foo=bar"); - const content2 = await res2.text(); - assert(!content2.includes("@tailwind")); - }, { loadConfig: true }); -}); - -Deno.test("middleware test", async (t) => { - await withFakeServe( - "./tests/fixture_base_path/dev.ts", - async (server) => { - await t.step("expected root", async () => { - const res = await server.get("/foo/bar"); - const content = await res.text(); - assertEquals(res.headers.get("server"), "fresh server"); - assertStringIncludes(content, "middleware is working"); - }); - - await t.step("redirect root", async () => { - const res = await server.get(""); - const content = await res.text(); - assertEquals(res.headers.get("server"), "fresh server"); - assertStringIncludes(content, "middleware is working"); - }); - - await t.step("miiddleware before an invalid route", async () => { - const res = await server.get("/asdfasdfasdfasdfasdfasdf"); - assertEquals(res.headers.get("server"), "fresh server"); - await res.body?.cancel(); - }); - }, - { loadConfig: true }, - ); -}); - -Deno.test("TailwindCSS - build mode", async () => { - await runBuild("./tests/fixture_base_path_build/dev.ts"); - await withFakeServe( - "./tests/fixture_base_path_build/main.ts", - async (server) => { - const res = await server.get("/styles.css"); - const content = await res.text(); - assertStringIncludes(content, ".text-red-600{"); - }, - { loadConfig: true }, - ); -}); - -Deno.test("TailwindCSS - config", async () => { - await withFakeServe( - "./tests/fixture_base_path_config/dev.ts", - async (server) => { - const res = await server.get("/styles.css"); - const content = await res.text(); - assertStringIncludes(content, ".text-pp"); - }, - { loadConfig: true }, - ); -}); - -Deno.test("TailwindCSS - middleware only css", async () => { - await withFakeServe( - "./tests/fixture_base_path/dev.ts", - async (server) => { - const res = await server.get("/middleware-only.css"); - const content = await res.text(); - assertStringIncludes(content, ".foo-bar"); - }, - { loadConfig: true }, - ); -}); - -function extractImportUrls(scriptContent: string): string[] { - const project = new Project({ - useInMemoryFileSystem: true, - }); - - const sourceFile = project.createSourceFile("script.js", scriptContent); - - const importDeclarations = sourceFile.getImportDeclarations(); - - return importDeclarations.map((importDeclaration) => - importDeclaration.getModuleSpecifierValue() - ); -} - -async function checkPreloadLinks(page: Page, basePath: string) { - const preloadLinks: string[] = await page.$$eval( - 'link[rel="modulepreload"]', - (links) => links.map((link) => link.getAttribute("href")), - ); - assert(preloadLinks.length > 0, "No preload links found"); - preloadLinks.forEach((href) => { - assert( - href.startsWith(basePath), - `Preload link ${href} does not include the correct base path`, - ); - }); -} - -async function checkScriptSrcs(page: Page, basePath: string) { - const scriptSrcs: string[] = await page.$$eval( - "script[src]", - (scripts) => scripts.map((script) => script.getAttribute("src")), - ); - assert(scriptSrcs.length > 0, "No script srcs found"); - scriptSrcs.forEach((src) => { - assert( - src.startsWith(basePath), - `Script src ${src} does not include the correct base path`, - ); - }); -} - -async function checkInlineScripts(page: Page, basePath: string) { - const inlineScripts = await page.$$eval( - "script:not([src])", - (scripts) => scripts.map((script) => script.textContent), - ); - assert(inlineScripts.length > 0, "No inline scripts found"); - inlineScripts.forEach((scriptContent) => { - const importUrls = extractImportUrls(scriptContent); - importUrls.forEach((url) => { - assert( - url.startsWith(basePath), - `Import URL ${url} does not include the correct base path`, - ); - }); - }); -} diff --git a/tests/build_test.ts b/tests/build_test.ts deleted file mode 100644 index ab327a98545..00000000000 --- a/tests/build_test.ts +++ /dev/null @@ -1,307 +0,0 @@ -import * as path from "$std/path/mod.ts"; -import { - assert, - assertEquals, - assertNotMatch, - assertStringIncludes, - puppeteer, -} from "./deps.ts"; -import { - getStdOutput, - recreateFolder, - startFreshServer, - waitForText, - withFakeServe, -} from "./test_utils.ts"; -import { BuildSnapshotJson } from "../src/build/mod.ts"; - -function runBuild(fixture: string, subDirPath: string, outDir: string) { - return new Deno.Command(Deno.execPath(), { - args: [ - "run", - "-A", - path.join(fixture, subDirPath, "dev.ts"), - "build", - ], - env: { - GITHUB_SHA: "__BUILD_ID__", - DENO_DEPLOYMENT_ID: "__BUILD_ID__", - FRESH_TEST_OUTDIR: outDir, - }, - stdin: "null", - stdout: "piped", - stderr: "piped", - }).output(); -} - -async function testBuild( - t: Deno.TestContext, - fixture: string, - options: { - subDirPath?: string; - outDir?: string; - } = {}, -) { - const subDirPath = options.subDirPath ?? ""; - const outDir = options.outDir ?? path.join(fixture, subDirPath, "_fresh"); - - try { - await t.step("build snapshot", async () => { - const res = await runBuild(fixture, subDirPath, outDir); - const { stdout } = getStdOutput(res); - assert( - !/Using snapshot found at/.test(stdout), - "Using snapshot message was shown during build", - ); - - assert((await Deno.stat(outDir)).isDirectory, "Missing output directory"); - }); - - const snapshot = JSON.parse( - await Deno.readTextFile(path.join(outDir, "snapshot.json")), - ) as BuildSnapshotJson; - - await t.step("check snapshot file", async () => { - assert( - Array.isArray(snapshot.files["island-counter.js"]), - "Island output file not found in snapshot", - ); - assert( - Array.isArray(snapshot.files["main.js"]), - "main.js output file not found in snapshot", - ); - assert( - Array.isArray(snapshot.files["signals.js"]), - "signals.js output file not found in snapshot", - ); - - // Should not include `preact/debug` - const mainJs = await Deno.readTextFile(path.join(outDir, "main.js")); - assertNotMatch(mainJs, /Undefined parent passed to render()/); - }); - - await t.step("restore from snapshot", async () => { - const { lines, serverProcess, address, output } = await startFreshServer({ - args: [ - "run", - "-A", - path.join(fixture, subDirPath, "main.ts"), - ], - }); - - // Check if restore snapshot message was printed - assert( - output.find((line) => line.includes("Using snapshot found at")), - "Did not print restoring from snapshot line", - ); - - try { - const browser = await puppeteer.launch({ args: ["--no-sandbox"] }); - - try { - const page = await browser.newPage(); - await page.goto(address); - - await page.waitForSelector("button:not([disabled])"); - await page.click("button"); - - await waitForText(page, "p", "1"); - - // Ensure that it uses the build id from the snapshot - const assetUrls = await page.evaluate(() => { - const links = Array.from(document.querySelectorAll("link")).map( - (link) => link.href, - ); - const scripts = Array.from(document.querySelectorAll("script")) - .filter((script) => - script.src && !script.src.endsWith("refresh.js") - ).map((script) => script.src); - - return [...links, ...scripts]; - }); - - for (let i = 0; i < assetUrls.length; i++) { - assertStringIncludes(assetUrls[i], snapshot.build_id); - } - } finally { - await browser.close(); - } - } finally { - serverProcess.kill("SIGTERM"); - await serverProcess.status; - - // Drain the lines stream - for await (const _ of lines) { /* noop */ } - } - }); - - await t.step("should not restore from snapshot in dev mode", async () => { - const { lines, serverProcess, output } = await startFreshServer({ - args: [ - "run", - "-A", - path.join(fixture, subDirPath, "dev.ts"), - ], - }); - - try { - // Check that restore snapshot message was NOT printed - assert( - !output.find((line) => line.includes("Using snapshot found at")), - "Restoring from snapshot message should not appear in dev mode", - ); - } finally { - serverProcess.kill("SIGTERM"); - await serverProcess.status; - - // Drain the lines stream - for await (const _ of lines) { /* noop */ } - } - }); - } finally { - await Deno.remove(path.join(fixture, subDirPath, "_fresh"), { - recursive: true, - }); - } -} - -Deno.test("build snapshot and restore from it", async (t) => { - // Note: If you change the fixture_build directory, you must also update fixture_build_sub_dir - const fixture = path.join(Deno.cwd(), "tests", "fixture_build"); - await testBuild(t, fixture); -}); - -Deno.test("build snapshot and restore from it when has sub dirs", async (t) => { - const fixture = path.join(Deno.cwd(), "tests", "fixture_build_sub_dir"); - await testBuild(t, fixture, { subDirPath: "src" }); -}); - -Deno.test( - "build snapshot with custom build.outDir", - async (t) => { - async function assertOutputDir(outDir: string, out: Deno.CommandOutput) { - const { stdout, stderr } = getStdOutput(out); - - const msg = - `Missing output directory: ${outDir}\n\nCLI output:\n${stdout}\n${stderr}`; - - const dir: string | URL = outDir.startsWith("file://") - ? new URL(outDir) - : outDir; - try { - assert((await Deno.stat(dir)).isDirectory, msg); - } catch (err) { - throw new Error(msg, { cause: err }); - } - } - - await t.step("uses on relative outDir", async () => { - const fixture = path.join(Deno.cwd(), "tests", "fixture_build_out_dir"); - const out = await runBuild(fixture, "", "./tmp/asdf"); - const outDir = path.join(fixture, "tmp", "asdf"); - - await assertOutputDir(outDir, out); - }); - - await t.step("uses absolute outDir", async () => { - const fixture = path.join( - Deno.cwd(), - "tests", - "fixture_build_out_dir_sub", - ); - - const outDir = path.join(fixture, "tmp"); - const out = await runBuild(fixture, "src", outDir); - - await assertOutputDir(outDir, out); - }); - - await t.step("uses file:// outDir", async () => { - const fixture = path.join( - Deno.cwd(), - "tests", - "fixture_build_out_dir_sub2", - ); - - const outDirPath = path.join(fixture, "tmp"); - const outDir = path.toFileUrl(outDirPath).href; - const out = await runBuild(fixture, "src", outDir); - await assertOutputDir(outDir, out); - }); - }, -); - -Deno.test("pass target options", async () => { - const fixture = path.join(Deno.cwd(), "tests", "fixture_build_target"); - const out = await new Deno.Command(Deno.execPath(), { - args: [ - "run", - "-A", - path.join(fixture, "dev.ts"), - "build", - ], - env: { - GITHUB_SHA: "__BUILD_ID__", - DENO_DEPLOYMENT_ID: "__BUILD_ID__", - FRESH_TEST_TARGET: "es2015", - }, - stdin: "null", - stdout: "piped", - stderr: "piped", - }).output(); - - const { stdout, stderr } = getStdOutput(out); - const txt = await Deno.readTextFile( - path.join(fixture, "_fresh", "island-counter.js"), - ); - - assertNotMatch( - txt, - /\?\?/, - `Asset contained ?? despite target es2015\n\n${stdout}\n${stderr}`, - ); -}); - -Deno.test("serve static files from build dir", async () => { - const filePath = path.join( - path.dirname(path.fromFileUrl(import.meta.url)), - "fixture_build_static", - "_fresh", - "static", - "foo.txt", - ); - - await recreateFolder(path.dirname(filePath)); - await Deno.writeTextFile(filePath, `it works`); - - await withFakeServe( - "./tests/fixture_build_static/main.ts", - async (server) => { - const res = await server.get("/foo.txt"); - const text = await res.text(); - assertEquals(text, "it works"); - }, - ); -}); - -Deno.test("prefer static files from build dir", async () => { - const filePath = path.join( - path.dirname(path.fromFileUrl(import.meta.url)), - "fixture_build_static", - "_fresh", - "static", - "duplicate.txt", - ); - - await recreateFolder(path.dirname(filePath)); - await Deno.writeTextFile(filePath, "it works"); - - await withFakeServe( - "./tests/fixture_build_static/main.ts", - async (server) => { - const res = await server.get("/duplicate.txt"); - const text = await res.text(); - assertEquals(text, "it works"); - }, - ); -}); diff --git a/tests/cli_update_check_test.ts b/tests/cli_update_check_test.ts deleted file mode 100644 index 89771e56bed..00000000000 --- a/tests/cli_update_check_test.ts +++ /dev/null @@ -1,361 +0,0 @@ -import { join } from "../src/server/deps.ts"; -import { - assert, - assertEquals, - assertMatch, - assertNotEquals, - assertNotMatch, -} from "./deps.ts"; -import versions from "../versions.json" with { type: "json" }; -import { CheckFile } from "../src/dev/update_check.ts"; -import { WEEK } from "../src/dev/deps.ts"; -import { getStdOutput } from "../tests/test_utils.ts"; - -Deno.test({ - name: "stores update check file in $HOME/fresh", - async fn() { - const tmpDirName = await Deno.makeTempDir(); - const filePath = join(tmpDirName, "latest.json"); - - await new Deno.Command(Deno.execPath(), { - args: ["run", "-A", "./tests/fixture_update_check/mod.ts"], - env: { - CI: "false", - TEST_HOME: tmpDirName, - }, - }).output(); - - const text = JSON.parse(await Deno.readTextFile(filePath)); - assertEquals(text, { - current_version: versions[0], - latest_version: "99.99.999", - last_checked: text.last_checked, - last_shown: text.last_shown, - }); - - await Deno.remove(tmpDirName, { recursive: true }); - }, -}); - -Deno.test({ - name: "skips update check on specific environment variables", - async fn(t) { - const envs = ["FRESH_NO_UPDATE_CHECK", "CI", "DENO_DEPLOYMENT_ID"]; - - for (const env of envs) { - await t.step(`checking ${env}`, async () => { - const tmpDirName = await Deno.makeTempDir(); - const out = await new Deno.Command(Deno.execPath(), { - args: ["run", "-A", "./tests/fixture_update_check/mod.ts"], - env: { - [env]: "true", - TEST_HOME: tmpDirName, - LATEST_VERSION: "1.30.0", - }, - stderr: "piped", - stdout: "piped", - }).output(); - - const { stdout } = getStdOutput(out); - assertNotMatch(stdout, /Fresh 1\.30\.0 is available/); - - await Deno.remove(tmpDirName, { recursive: true }); - }); - } - }, -}); - -Deno.test({ - name: "shows update message on version mismatch", - async fn() { - const tmpDirName = await Deno.makeTempDir(); - const filePath = join(tmpDirName, "latest.json"); - - await Deno.writeTextFile( - filePath, - JSON.stringify({ - current_version: "1.1.0", - latest_version: "1.1.0", - last_checked: new Date(0).toISOString(), - }), - ); - - const out = await new Deno.Command(Deno.execPath(), { - args: ["run", "-A", "./tests/fixture_update_check/mod.ts"], - env: { - CI: "false", - TEST_HOME: tmpDirName, - LATEST_VERSION: "999.999.0", - }, - stderr: "piped", - stdout: "piped", - }).output(); - - const { stdout } = getStdOutput(out); - assertMatch(stdout, /Fresh 999\.999\.0 is available/); - - // Updates check file - const text = JSON.parse(await Deno.readTextFile(filePath)); - assertEquals(text, { - current_version: versions[0], - latest_version: "999.999.0", - last_checked: text.last_checked, - last_shown: text.last_shown, - }); - - await Deno.remove(tmpDirName, { recursive: true }); - }, -}); - -Deno.test({ - name: "only fetch new version defined by interval", - async fn(t) { - const tmpDirName = await Deno.makeTempDir(); - - await t.step("fetches latest version initially", async () => { - const out = await new Deno.Command(Deno.execPath(), { - args: ["run", "-A", "./tests/fixture_update_check/mod.ts"], - env: { - CI: "false", - UPDATE_INTERVAL: "100000", - TEST_HOME: tmpDirName, - LATEST_VERSION: "1.30.0", - }, - stderr: "piped", - stdout: "piped", - }).output(); - - const { stdout } = getStdOutput(out); - assertMatch(stdout, /fetching latest version/); - }); - - await t.step("should not fetch if interval has not passed", async () => { - const out = await new Deno.Command(Deno.execPath(), { - args: ["run", "-A", "./tests/fixture_update_check/mod.ts"], - env: { - CI: "false", - UPDATE_INTERVAL: "100000", - TEST_HOME: tmpDirName, - LATEST_VERSION: "1.30.0", - }, - stderr: "piped", - stdout: "piped", - }).output(); - - const { stdout } = getStdOutput(out); - assertNotMatch(stdout, /fetching latest version/); - }); - - await t.step("fetches if interval has passed", async () => { - const out = await new Deno.Command(Deno.execPath(), { - args: ["run", "-A", "./tests/fixture_update_check/mod.ts"], - env: { - CI: "false", - UPDATE_INTERVAL: "1 ", - TEST_HOME: tmpDirName, - LATEST_VERSION: "1.30.0", - }, - }).output(); - - const { stdout } = getStdOutput(out); - assertMatch(stdout, /fetching latest version/); - }); - - await Deno.remove(tmpDirName, { recursive: true }); - }, -}); - -Deno.test({ - name: "updates current version in cache file", - async fn() { - const tmpDirName = await Deno.makeTempDir(); - - const checkFile: CheckFile = { - current_version: "1.2.0", - latest_version: "1.2.0", - last_checked: new Date(Date.now() - WEEK).toISOString(), - }; - - await Deno.writeTextFile( - join(tmpDirName, "latest.json"), - JSON.stringify(checkFile, null, 2), - ); - - const out = await new Deno.Command(Deno.execPath(), { - args: ["run", "-A", "./tests/fixture_update_check/mod.ts"], - env: { - CI: "false", - TEST_HOME: tmpDirName, - LATEST_VERSION: versions[0], - }, - stderr: "piped", - stdout: "piped", - }).output(); - - const { stdout } = getStdOutput(out); - assertNotMatch(stdout, /Fresh .* is available/); - - await Deno.remove(tmpDirName, { recursive: true }); - }, -}); - -Deno.test({ - name: "only shows update message when current < latest", - async fn() { - const tmpDirName = await Deno.makeTempDir(); - - const checkFile: CheckFile = { - current_version: "9999.999.0", - latest_version: "1.2.0", - last_checked: new Date().toISOString(), - }; - - await Deno.writeTextFile( - join(tmpDirName, "latest.json"), - JSON.stringify(checkFile, null, 2), - ); - - const out = await new Deno.Command(Deno.execPath(), { - args: ["run", "-A", "./tests/fixture_update_check/mod.ts"], - env: { - CI: "false", - TEST_HOME: tmpDirName, - LATEST_VERSION: versions[0], - CURRENT_VERSION: "99999.9999.0", - }, - stderr: "piped", - stdout: "piped", - }).output(); - - const { stdout } = getStdOutput(out); - assertNotMatch(stdout, /Fresh .* is available/); - - await Deno.remove(tmpDirName, { recursive: true }); - }, -}); - -Deno.test("migrates to last_shown property", async () => { - const tmpDirName = await Deno.makeTempDir(); - - const checkFile: CheckFile = { - latest_version: "1.4.0", - current_version: "1.2.0", - last_checked: new Date().toISOString(), - }; - - await Deno.writeTextFile( - join(tmpDirName, "latest.json"), - JSON.stringify(checkFile, null, 2), - ); - - const out = await new Deno.Command(Deno.execPath(), { - args: ["run", "-A", "./tests/fixture_update_check/mod.ts"], - env: { - CI: "false", - TEST_HOME: tmpDirName, - CURRENT_VERSION: "1.2.0", - LATEST_VERSION: "99999.9999.0", - }, - stderr: "piped", - stdout: "piped", - }).output(); - - const { stdout } = getStdOutput(out); - assertMatch(stdout, /Fresh .* is available/); - - const checkFileAfter = JSON.parse( - await Deno.readTextFile( - join(tmpDirName, "latest.json"), - ), - ); - - assert( - typeof checkFileAfter.last_shown === "string", - "Did not write last_shown " + JSON.stringify(checkFileAfter, null, 2), - ); - - await Deno.remove(tmpDirName, { recursive: true }); -}); - -Deno.test("doesn't show update if last_shown + interval >= today", async () => { - const tmpDirName = await Deno.makeTempDir(); - - const todayMinus1Hour = new Date(); - todayMinus1Hour.setHours(todayMinus1Hour.getHours() - 1); - - const checkFile: CheckFile = { - current_version: "1.2.0", - latest_version: "1.6.0", - last_checked: new Date().toISOString(), - last_shown: todayMinus1Hour.toISOString(), - }; - - await Deno.writeTextFile( - join(tmpDirName, "latest.json"), - JSON.stringify(checkFile, null, 2), - ); - - const out = await new Deno.Command(Deno.execPath(), { - args: ["run", "-A", "./tests/fixture_update_check/mod.ts"], - env: { - CI: "false", - TEST_HOME: tmpDirName, - CURRENT_VERSION: "1.2.0", - LATEST_VERSION: "99999.9999.0", - }, - stderr: "piped", - stdout: "piped", - }).output(); - - const { stdout } = getStdOutput(out); - assertNotMatch(stdout, /Fresh .* is available/); - - await Deno.remove(tmpDirName, { recursive: true }); -}); - -Deno.test( - "shows update if last_shown + interval < today", - async () => { - const tmpDirName = await Deno.makeTempDir(); - - const yesterday = new Date(); - yesterday.setDate(yesterday.getDate() - 1); - - const checkFile: CheckFile = { - current_version: "1.2.0", - latest_version: "1.8.0", - last_checked: new Date().toISOString(), - last_shown: yesterday.toISOString(), - }; - - await Deno.writeTextFile( - join(tmpDirName, "latest.json"), - JSON.stringify(checkFile, null, 2), - ); - - const out = await new Deno.Command(Deno.execPath(), { - args: ["run", "-A", "./tests/fixture_update_check/mod.ts"], - env: { - CI: "false", - TEST_HOME: tmpDirName, - CURRENT_VERSION: versions[0], - LATEST_VERSION: "99999.9999.0", - }, - stderr: "piped", - stdout: "piped", - }).output(); - - const { stdout } = getStdOutput(out); - assertMatch(stdout, /Fresh .* is available/); - - const checkFileAfter = JSON.parse( - await Deno.readTextFile( - join(tmpDirName, "latest.json"), - ), - ); - - assertNotEquals(checkFileAfter.last_shown, yesterday.toISOString()); - - await Deno.remove(tmpDirName, { recursive: true }); - }, -); diff --git a/tests/deps.ts b/tests/deps.ts deleted file mode 100644 index 388537becd1..00000000000 --- a/tests/deps.ts +++ /dev/null @@ -1,52 +0,0 @@ -/// -/// -/// -/// -/// - -export { - assert, - assertEquals, - assertExists, - AssertionError, - assertMatch, - assertNotEquals, - assertNotMatch, - assertRejects, - assertStringIncludes, - assertThrows, -} from "https://deno.land/std@0.216.0/assert/mod.ts"; -export { assertSnapshot } from "https://deno.land/std@0.216.0/testing/snapshot.ts"; -export { - TextLineStream, -} from "https://deno.land/std@0.216.0/streams/text_line_stream.ts"; -export { delay } from "https://deno.land/std@0.216.0/async/delay.ts"; -export { retry } from "https://deno.land/std@0.216.0/async/retry.ts"; -export { - default as puppeteer, - Page, -} from "https://deno.land/x/puppeteer@16.2.0/mod.ts"; -export { - Document, - DOMParser, - HTMLElement, - HTMLMetaElement, -} from "https://esm.sh/linkedom@0.16.8"; -export { defineConfig, type Preset } from "https://esm.sh/@twind/core@1.1.3"; -export { default as presetTailwind } from "https://esm.sh/@twind/preset-tailwind@1.1.4"; -export { copy } from "https://deno.land/std@0.216.0/fs/mod.ts"; -export { - basename, - dirname, - extname, - fromFileUrl, - join, - relative, - SEPARATOR, - toFileUrl, -} from "https://deno.land/std@0.216.0/path/mod.ts"; -export * as JSONC from "https://deno.land/std@0.216.0/jsonc/mod.ts"; -export * as colors from "https://deno.land/std@0.216.0/fmt/colors.ts"; -export { STATUS_CODE } from "https://deno.land/std@0.216.0/http/status.ts"; -export { stripAnsiCode } from "https://deno.land/std@0.216.0/fmt/colors.ts"; -export { Project } from "https://deno.land/x/ts_morph@21.0.1/mod.ts"; diff --git a/tests/dev_command_test.ts b/tests/dev_command_test.ts deleted file mode 100644 index fcd7eb65770..00000000000 --- a/tests/dev_command_test.ts +++ /dev/null @@ -1,156 +0,0 @@ -import { assert, assertEquals, assertStringIncludes } from "./deps.ts"; -import { STATUS_CODE } from "../server.ts"; -import { - assertNotSelector, - assertTextMany, - assertTextMatch, - fetchHtml, - getErrorOverlay, - waitForStyle, - withFakeServe, - withFresh, - withPageName, -} from "./test_utils.ts"; - -Deno.test({ - name: "dev_command config", - async fn() { - await withPageName( - "./tests/fixture_dev_config/main.ts", - async (page, address) => { - await page.goto(`${address}`); - await waitForStyle(page, "h1", "color", "rgb(220, 38, 38)"); - }, - ); - }, -}); - -Deno.test("dev_command config: shows codeframe", async () => { - await withFakeServe( - "./tests/fixture_dev_config/dev.ts", - async (server) => { - const { codeFrame } = await getErrorOverlay(server, "/codeframe"); - assert(codeFrame); - }, - ); -}); - -Deno.test("dev_command legacy", async () => { - await withPageName( - "./tests/fixture_dev_legacy/main.ts", - async (page, address) => { - await page.goto(`${address}`); - await waitForStyle(page, "h1", "color", "rgb(220, 38, 38)"); - }, - ); -}); - -Deno.test("dev_command legacy: shows codeframe", async () => { - await withFakeServe( - "./tests/fixture_dev_legacy/dev.ts", - async (server) => { - const { codeFrame } = await getErrorOverlay(server, "/codeframe"); - assert(codeFrame); - }, - ); -}); - -Deno.test("preact/debug is active in dev mode", async () => { - await withFakeServe( - "./tests/fixture_render_error/dev.ts", - async (server) => { - // SSR error is shown - const resp = await server.get("/"); - await resp.text(); // Consume - assertEquals(resp.status, STATUS_CODE.InternalServerError); - - const { title } = await getErrorOverlay(server, "/"); - assertStringIncludes(title, "Objects are not valid as a child"); - }, - ); -}); - -Deno.test("warns when using hooks in server components", async (t) => { - await withFakeServe("./tests/fixture/main.ts", async (server) => { - await t.step("useState", async () => { - const doc = await server.getHtml(`/hooks-server/useState`); - assertTextMatch(doc, "p", /Hook "useState" cannot be used/); - // Check for hint - assertTextMatch(doc, "p", /Instead, use the "useSignal" hook/); - }); - - await t.step("useReducer", async () => { - const doc = await server.getHtml(`/hooks-server/useReducer`); - assertTextMatch(doc, "p", /Hook "useReducer" cannot be used/); - }); - - // Valid - await t.step("does not warn in island", async () => { - const doc = await server.getHtml(`/hooks-server/island`); - assertTextMany(doc, "p", ["0"]); - }); - }); -}); - -Deno.test("shows custom 500 page for rendering errors when not in dev", async (t) => { - await withFresh({ - name: "./tests/fixture/main.ts", - options: { - env: { - DENO_DEPLOYMENT_ID: "foo", - }, - }, - }, async (address) => { - await t.step("useState", async () => { - const doc = await fetchHtml(`${address}/hooks-server/useState`); - assertNotSelector(doc, "pre"); - }); - - await t.step("useReducer", async () => { - const doc = await fetchHtml(`${address}/hooks-server/useReducer`); - assertNotSelector(doc, "pre"); - }); - }); -}); - -Deno.test("show codeframe in dev mode even with custom 500", async () => { - await withFakeServe( - "./tests/fixture_dev_codeframe/dev.ts", - async (server) => { - const { title } = await getErrorOverlay(server, "/"); - assertEquals(title, "fail"); - }, - ); - - await withFakeServe( - "./tests/fixture_dev_codeframe/main.ts", - async (server) => { - const doc = await server.getHtml("/"); - assertNotSelector(doc, "#fresh-error-overlay"); - }, - ); -}); - -Deno.test("serve client script source map", async () => { - await withFakeServe( - "./tests/fixture/dev.ts", - async (server) => { - const res = await server.get(`/_frsh/fresh_dev_client.js`); - await res.text(); // Consume body - assertEquals(res.status, 200); - assertEquals( - res.headers.get("Content-Type"), - "application/javascript; charset=UTF-8", - ); - - const res2 = await server.get(`/_frsh/fresh_dev_client.js.map`); - const json = await res2.json(); - assertEquals(res2.status, 200); - assertEquals( - res2.headers.get("Content-Type"), - "application/json; charset=UTF-8", - ); - assert(typeof json.mappings, "string"); - }, - ); -}); diff --git a/tests/dev_test.ts b/tests/dev_test.ts deleted file mode 100644 index 0c82e293b39..00000000000 --- a/tests/dev_test.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { collect } from "../src/dev/mod.ts"; -import { assert, dirname, fromFileUrl, join } from "./deps.ts"; - -Deno.test({ - name: "routes collect", - fn: async () => { - const { routes } = await collect( - join(dirname(fromFileUrl(import.meta.url)), "fixture"), - ); - - assert( - !routes.includes("routes/not_found.test.ts") && - !routes.includes("routes\\not_found.test.ts"), - ); - assert( - !routes.includes("routes/_404_test.tsx") && - !routes.includes("routes\\_404_test.tsx"), - ); - assert( - !routes.includes("routes/islands/test_test.tsx") && - !routes.includes("routes\\islands\\test_test.tsx"), - ); - }, -}); - -Deno.test({ - name: "routes collect with custom pattern", - fn: async () => { - const { routes } = await collect( - join( - dirname(fromFileUrl(import.meta.url)), - "fixture_router_ignore_files", - ), - /[\.|_]cy\.[t|j]s(x)?$/, - ); - - assert( - !routes.includes("routes/index.cy.ts") && - !routes.includes("routes\\index.cy.ts"), - ); - assert( - routes.includes("routes/index.tsx") || - routes.includes("routes\\index.tsx"), - ); - }, -}); diff --git a/tests/error_test.ts b/tests/error_test.ts deleted file mode 100644 index 3b716fa804e..00000000000 --- a/tests/error_test.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { STATUS_CODE } from "../server.ts"; -import { - assertEquals, - AssertionError, - assertRejects, - assertStringIncludes, -} from "./deps.ts"; -import { getErrorOverlay, withFakeServe } from "./test_utils.ts"; - -Deno.test("error page rendered", async () => { - await withFakeServe("./tests/fixture_error/dev.ts", async (server) => { - const resp = await server.get("/"); - assertEquals(resp.status, STATUS_CODE.InternalServerError); - assertEquals(resp.headers.get("content-type"), "text/html; charset=utf-8"); - const body = await resp.text(); - assertStringIncludes(body, "

500 page

"); - - const { title, stack } = await getErrorOverlay(server, "/"); - assertStringIncludes(title, `boom!`); - assertStringIncludes(stack, `at render`); - }); -}); - -Deno.test("error page rendered without error overlay", async () => { - await withFakeServe("./tests/fixture_error/main.ts", async (server) => { - const resp = await server.get("/"); - assertEquals(resp.status, STATUS_CODE.InternalServerError); - assertEquals(resp.headers.get("content-type"), "text/html; charset=utf-8"); - const body = await resp.text(); - assertStringIncludes(body, "

500 page

"); - - await assertRejects( - () => getErrorOverlay(server, "/"), - AssertionError, - undefined, - "Missing fresh error overlay", - ); - }); -}); diff --git a/tests/explicit_app_template_test.ts b/tests/explicit_app_template_test.ts deleted file mode 100644 index c92bb4384e3..00000000000 --- a/tests/explicit_app_template_test.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { - assertNotSelector, - assertSelector, - assertTextMany, - withFakeServe, -} from "./test_utils.ts"; -import { assertNotMatch } from "./deps.ts"; - -Deno.test("doesn't apply internal app template", async () => { - await withFakeServe( - "./tests/fixture_explicit_app/main.ts", - async (server) => { - const doc = await server.getHtml(`/`); - - // Doesn't render internal app template - assertNotSelector(doc, "body body"); - - assertSelector(doc, "html > head"); - assertSelector(doc, "html > body"); - assertSelector(doc, `meta[charset="utf-8"]`); - assertSelector( - doc, - `meta[name="viewport"][content="width=device-width, initial-scale=1.0"]`, - ); - assertTextMany(doc, "title", ["fresh title"]); - - // Still renders page - assertSelector(doc, "body > .inner-body > .page"); - }, - ); -}); - -Deno.test("user _app works with ", async () => { - await withFakeServe( - "./tests/fixture_explicit_app/main.ts", - async (server) => { - const doc = await server.getHtml(`/head`); - - // Doesn't render internal app template - assertNotSelector(doc, "body body"); - - assertSelector(doc, "html > head"); - assertSelector(doc, "html > body"); - assertSelector(doc, `meta[charset="utf-8"]`); - assertSelector( - doc, - `meta[name="viewport"][content="width=device-width, initial-scale=1.0"]`, - ); - assertSelector( - doc, - `meta[name="fresh"][content="test"]`, - ); - - // Still renders page - assertSelector(doc, "body > .inner-body > .page"); - }, - ); -}); - -Deno.test("don't duplicate ", async () => { - await withFakeServe( - "./tests/fixture_explicit_app/main.ts", - async (server) => { - const doc = await server.getHtml(`/title`); - assertTextMany(doc, "title", ["foo bar"]); - }, - ); -}); - -Deno.test("sets <html> + <head> + <body> classes", async () => { - await withFakeServe( - "./tests/fixture_explicit_app/main.ts", - async (server) => { - const doc = await server.getHtml(``); - assertSelector(doc, "html.html"); - assertSelector(doc, "head.head"); - assertSelector(doc, "body.body"); - }, - ); -}); - -// Issue: https://github.com/denoland/fresh/issues/1666 -Deno.test("renders valid html document", async () => { - await withFakeServe( - "./tests/fixture_explicit_app/main.ts", - async (server) => { - const res = await server.get("/"); - const text = await res.text(); - - assertNotMatch(text, /<\/body><\/head>/); - }, - ); -}); diff --git a/tests/fixture/custom_static/custom.txt b/tests/fixture/custom_static/custom.txt deleted file mode 100644 index 87245193225..00000000000 --- a/tests/fixture/custom_static/custom.txt +++ /dev/null @@ -1 +0,0 @@ -dir \ No newline at end of file diff --git a/tests/fixture/deno.json b/tests/fixture/deno.json deleted file mode 100644 index 07f66b10457..00000000000 --- a/tests/fixture/deno.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "lock": false, - "imports": { - "$fresh/": "../../", - "preact": "https://esm.sh/preact@10.22.0", - "preact/": "https://esm.sh/preact@10.22.0/", - "@preact/signals": "https://esm.sh/*@preact/signals@1.1.5", - "@preact/signals-core": "https://esm.sh/@preact/signals-core@1.3.1" - }, - "compilerOptions": { - "jsx": "react-jsx", - "jsxImportSource": "preact" - } -} diff --git a/tests/fixture/dev.ts b/tests/fixture/dev.ts deleted file mode 100755 index 2d85d6c183c..00000000000 --- a/tests/fixture/dev.ts +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env -S deno run -A --watch=static/,routes/ - -import dev from "$fresh/dev.ts"; - -await dev(import.meta.url, "./main.ts"); diff --git a/tests/fixture/fresh.config.ts b/tests/fixture/fresh.config.ts deleted file mode 100644 index 252cb21333c..00000000000 --- a/tests/fixture/fresh.config.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { FreshConfig } from "$fresh/server.ts"; - -export default { - async render(_ctx, render) { - await new Promise<void>((r) => r()); - const body = render(); - if (typeof body !== "string") { - throw new Error("body is missing"); - } - }, -} as FreshConfig; diff --git a/tests/fixture/fresh.gen.ts b/tests/fixture/fresh.gen.ts deleted file mode 100644 index e60cd48d357..00000000000 --- a/tests/fixture/fresh.gen.ts +++ /dev/null @@ -1,238 +0,0 @@ -// DO NOT EDIT. This file is generated by Fresh. -// This file SHOULD be checked into source version control. -// This file is automatically updated during development when running `dev.ts`. - -import * as $_404_from_middleware_throw_middleware from "./routes/404-from-middleware-throw/_middleware.ts"; -import * as $_404_from_middleware_throw_index from "./routes/404-from-middleware-throw/index.tsx"; -import * as $_404_from_middleware_middleware from "./routes/404-from-middleware/_middleware.ts"; -import * as $_404_from_middleware_index from "./routes/404-from-middleware/index.tsx"; -import * as $_404_from_throw from "./routes/404_from_throw.tsx"; -import * as $_name_ from "./routes/[name].tsx"; -import * as $_404 from "./routes/_404.tsx"; -import * as $_500 from "./routes/_500.tsx"; -import * as $_app from "./routes/_app.tsx"; -import * as $_middleware from "./routes/_middleware.ts"; -import * as $admin_site_ from "./routes/admin/[site].tsx"; -import * as $api_get_only from "./routes/api/get_only.ts"; -import * as $api_head_override from "./routes/api/head_override.ts"; -import * as $assetsCaching_index from "./routes/assetsCaching/index.tsx"; -import * as $books_id_ from "./routes/books/[id].tsx"; -import * as $connInfo from "./routes/connInfo.ts"; -import * as $ctx_config from "./routes/ctx_config.tsx"; -import * as $ctx_config_props from "./routes/ctx_config_props.tsx"; -import * as $error_boundary from "./routes/error_boundary.tsx"; -import * as $event_handler_string from "./routes/event_handler_string.tsx"; -import * as $event_handler_string_island from "./routes/event_handler_string_island.tsx"; -import * as $evil from "./routes/evil.tsx"; -import * as $failure from "./routes/failure.ts"; -import * as $foo_bar_baz from "./routes/foo.bar.baz.tsx"; -import * as $foo_bar from "./routes/foo.bar.tsx"; -import * as $head_deduplicate from "./routes/head_deduplicate.tsx"; -import * as $hooks_server_island from "./routes/hooks-server/island.tsx"; -import * as $hooks_server_useReducer from "./routes/hooks-server/useReducer.tsx"; -import * as $hooks_server_useState from "./routes/hooks-server/useState.tsx"; -import * as $i18n_lang_lang from "./routes/i18n/[[lang]]/lang.tsx"; -import * as $index from "./routes/index.tsx"; -import * as $intercept from "./routes/intercept.tsx"; -import * as $intercept_args from "./routes/intercept_args.tsx"; -import * as $island_json from "./routes/island_json.tsx"; -import * as $islands_index from "./routes/islands/index.tsx"; -import * as $islands_multiple_island_exports from "./routes/islands/multiple_island_exports.tsx"; -import * as $islands_returning_null from "./routes/islands/returning_null.tsx"; -import * as $islands_root_fragment from "./routes/islands/root_fragment.tsx"; -import * as $islands_root_fragment_conditional_first from "./routes/islands/root_fragment_conditional_first.tsx"; -import * as $layeredMdw_middleware from "./routes/layeredMdw/_middleware.ts"; -import * as $layeredMdw_layer2_no_mw_without_mw from "./routes/layeredMdw/layer2-no-mw/without_mw.ts"; -import * as $layeredMdw_layer2_with_params_tenantId_id_ from "./routes/layeredMdw/layer2-with-params/[tenantId]/[id].ts"; -import * as $layeredMdw_layer2_with_params_tenantId_middleware from "./routes/layeredMdw/layer2-with-params/[tenantId]/_middleware.ts"; -import * as $layeredMdw_layer2_with_params_middleware from "./routes/layeredMdw/layer2-with-params/_middleware.ts"; -import * as $layeredMdw_layer2_middleware from "./routes/layeredMdw/layer2/_middleware.ts"; -import * as $layeredMdw_layer2_abc from "./routes/layeredMdw/layer2/abc.ts"; -import * as $layeredMdw_layer2_index from "./routes/layeredMdw/layer2/index.ts"; -import * as $layeredMdw_layer2_layer3_id_ from "./routes/layeredMdw/layer2/layer3/[id].ts"; -import * as $layeredMdw_layer2_layer3_middleware from "./routes/layeredMdw/layer2/layer3/_middleware.ts"; -import * as $layeredMdw_nesting_tenant_environment_id_ from "./routes/layeredMdw/nesting/[tenant]/[environment]/[id].tsx"; -import * as $layeredMdw_nesting_tenant_environment_middleware from "./routes/layeredMdw/nesting/[tenant]/[environment]/_middleware.ts"; -import * as $layeredMdw_nesting_tenant_middleware from "./routes/layeredMdw/nesting/[tenant]/_middleware.ts"; -import * as $layeredMdw_nesting_middleware from "./routes/layeredMdw/nesting/_middleware.ts"; -import * as $middleware_error_handler_middleware from "./routes/middleware-error-handler/_middleware.ts"; -import * as $middleware_error_handler_index from "./routes/middleware-error-handler/index.tsx"; -import * as $middleware_root from "./routes/middleware_root.ts"; -import * as $movies_foo_json from "./routes/movies/[foo].json.ts"; -import * as $movies_foo_bar_ from "./routes/movies/[foo]@[bar].ts"; -import * as $nonce_inline from "./routes/nonce_inline.tsx"; -import * as $not_found from "./routes/not_found.ts"; -import * as $params from "./routes/params.tsx"; -import * as $preact_boolean_attrs from "./routes/preact/boolean_attrs.tsx"; -import * as $props_id_ from "./routes/props/[id].tsx"; -import * as $route_groups_islands_index from "./routes/route-groups-islands/index.tsx"; -import * as $route_groups_bar_baz_layout from "./routes/route-groups/(bar)/(baz)/_layout.tsx"; -import * as $route_groups_bar_baz_baz from "./routes/route-groups/(bar)/(baz)/baz.tsx"; -import * as $route_groups_bar_layout from "./routes/route-groups/(bar)/_layout.tsx"; -import * as $route_groups_bar_bar from "./routes/route-groups/(bar)/bar.tsx"; -import * as $route_groups_bar_boof_index from "./routes/route-groups/(bar)/boof/index.tsx"; -import * as $route_groups_foo_layout from "./routes/route-groups/(foo)/_layout.tsx"; -import * as $route_groups_foo_index from "./routes/route-groups/(foo)/index.tsx"; -import * as $signal_shared from "./routes/signal_shared.tsx"; -import * as $spoof_state from "./routes/spoof_state.tsx"; -import * as $state_in_props_middleware from "./routes/state-in-props/_middleware.ts"; -import * as $state_in_props_index from "./routes/state-in-props/index.tsx"; -import * as $state_middleware_middleware from "./routes/state-middleware/_middleware.ts"; -import * as $state_middleware_foo_middleware from "./routes/state-middleware/foo/_middleware.ts"; -import * as $state_middleware_foo_index from "./routes/state-middleware/foo/index.tsx"; -import * as $static from "./routes/static.tsx"; -import * as $status_overwrite from "./routes/status_overwrite.tsx"; -import * as $std from "./routes/std.tsx"; -import * as $umlaut_äöüß from "./routes/umlaut-äöüß.tsx"; -import * as $wildcard from "./routes/wildcard.tsx"; -import * as $Counter from "./islands/Counter.tsx"; -import * as $DangerousIsland from "./islands/DangerousIsland.tsx"; -import * as $Foo_Bar from "./islands/Foo.Bar.tsx"; -import * as $FormIsland from "./islands/FormIsland.tsx"; -import * as $Greeter from "./islands/Greeter.tsx"; -import * as $HookIsland from "./islands/HookIsland.tsx"; -import * as $JsonIsland from "./islands/JsonIsland.tsx"; -import * as $MultipleCounters from "./islands/MultipleCounters.tsx"; -import * as $ReturningNull from "./islands/ReturningNull.tsx"; -import * as $RootFragment from "./islands/RootFragment.tsx"; -import * as $RootFragmentWithConditionalFirst from "./islands/RootFragmentWithConditionalFirst.tsx"; -import * as $StringEventIsland from "./islands/StringEventIsland.tsx"; -import * as $Test from "./islands/Test.tsx"; -import * as $folder_Counter from "./islands/folder/Counter.tsx"; -import * as $folder_subfolder_Counter from "./islands/folder/subfolder/Counter.tsx"; -import * as $kebab_case_counter_test from "./islands/kebab-case-counter-test.tsx"; -import * as $route_groups_islands_islands_Counter from "./routes/route-groups-islands/(_islands)/Counter.tsx"; -import * as $route_groups_islands_islands_invalid from "./routes/route-groups-islands/(_islands)/invalid.tsx"; -import { type Manifest } from "$fresh/server.ts"; - -const manifest = { - routes: { - "./routes/404-from-middleware-throw/_middleware.ts": - $_404_from_middleware_throw_middleware, - "./routes/404-from-middleware-throw/index.tsx": - $_404_from_middleware_throw_index, - "./routes/404-from-middleware/_middleware.ts": - $_404_from_middleware_middleware, - "./routes/404-from-middleware/index.tsx": $_404_from_middleware_index, - "./routes/404_from_throw.tsx": $_404_from_throw, - "./routes/[name].tsx": $_name_, - "./routes/_404.tsx": $_404, - "./routes/_500.tsx": $_500, - "./routes/_app.tsx": $_app, - "./routes/_middleware.ts": $_middleware, - "./routes/admin/[site].tsx": $admin_site_, - "./routes/api/get_only.ts": $api_get_only, - "./routes/api/head_override.ts": $api_head_override, - "./routes/assetsCaching/index.tsx": $assetsCaching_index, - "./routes/books/[id].tsx": $books_id_, - "./routes/connInfo.ts": $connInfo, - "./routes/ctx_config.tsx": $ctx_config, - "./routes/ctx_config_props.tsx": $ctx_config_props, - "./routes/error_boundary.tsx": $error_boundary, - "./routes/event_handler_string.tsx": $event_handler_string, - "./routes/event_handler_string_island.tsx": $event_handler_string_island, - "./routes/evil.tsx": $evil, - "./routes/failure.ts": $failure, - "./routes/foo.bar.baz.tsx": $foo_bar_baz, - "./routes/foo.bar.tsx": $foo_bar, - "./routes/head_deduplicate.tsx": $head_deduplicate, - "./routes/hooks-server/island.tsx": $hooks_server_island, - "./routes/hooks-server/useReducer.tsx": $hooks_server_useReducer, - "./routes/hooks-server/useState.tsx": $hooks_server_useState, - "./routes/i18n/[[lang]]/lang.tsx": $i18n_lang_lang, - "./routes/index.tsx": $index, - "./routes/intercept.tsx": $intercept, - "./routes/intercept_args.tsx": $intercept_args, - "./routes/island_json.tsx": $island_json, - "./routes/islands/index.tsx": $islands_index, - "./routes/islands/multiple_island_exports.tsx": - $islands_multiple_island_exports, - "./routes/islands/returning_null.tsx": $islands_returning_null, - "./routes/islands/root_fragment.tsx": $islands_root_fragment, - "./routes/islands/root_fragment_conditional_first.tsx": - $islands_root_fragment_conditional_first, - "./routes/layeredMdw/_middleware.ts": $layeredMdw_middleware, - "./routes/layeredMdw/layer2-no-mw/without_mw.ts": - $layeredMdw_layer2_no_mw_without_mw, - "./routes/layeredMdw/layer2-with-params/[tenantId]/[id].ts": - $layeredMdw_layer2_with_params_tenantId_id_, - "./routes/layeredMdw/layer2-with-params/[tenantId]/_middleware.ts": - $layeredMdw_layer2_with_params_tenantId_middleware, - "./routes/layeredMdw/layer2-with-params/_middleware.ts": - $layeredMdw_layer2_with_params_middleware, - "./routes/layeredMdw/layer2/_middleware.ts": $layeredMdw_layer2_middleware, - "./routes/layeredMdw/layer2/abc.ts": $layeredMdw_layer2_abc, - "./routes/layeredMdw/layer2/index.ts": $layeredMdw_layer2_index, - "./routes/layeredMdw/layer2/layer3/[id].ts": $layeredMdw_layer2_layer3_id_, - "./routes/layeredMdw/layer2/layer3/_middleware.ts": - $layeredMdw_layer2_layer3_middleware, - "./routes/layeredMdw/nesting/[tenant]/[environment]/[id].tsx": - $layeredMdw_nesting_tenant_environment_id_, - "./routes/layeredMdw/nesting/[tenant]/[environment]/_middleware.ts": - $layeredMdw_nesting_tenant_environment_middleware, - "./routes/layeredMdw/nesting/[tenant]/_middleware.ts": - $layeredMdw_nesting_tenant_middleware, - "./routes/layeredMdw/nesting/_middleware.ts": - $layeredMdw_nesting_middleware, - "./routes/middleware-error-handler/_middleware.ts": - $middleware_error_handler_middleware, - "./routes/middleware-error-handler/index.tsx": - $middleware_error_handler_index, - "./routes/middleware_root.ts": $middleware_root, - "./routes/movies/[foo].json.ts": $movies_foo_json, - "./routes/movies/[foo]@[bar].ts": $movies_foo_bar_, - "./routes/nonce_inline.tsx": $nonce_inline, - "./routes/not_found.ts": $not_found, - "./routes/params.tsx": $params, - "./routes/preact/boolean_attrs.tsx": $preact_boolean_attrs, - "./routes/props/[id].tsx": $props_id_, - "./routes/route-groups-islands/index.tsx": $route_groups_islands_index, - "./routes/route-groups/(bar)/(baz)/_layout.tsx": - $route_groups_bar_baz_layout, - "./routes/route-groups/(bar)/(baz)/baz.tsx": $route_groups_bar_baz_baz, - "./routes/route-groups/(bar)/_layout.tsx": $route_groups_bar_layout, - "./routes/route-groups/(bar)/bar.tsx": $route_groups_bar_bar, - "./routes/route-groups/(bar)/boof/index.tsx": $route_groups_bar_boof_index, - "./routes/route-groups/(foo)/_layout.tsx": $route_groups_foo_layout, - "./routes/route-groups/(foo)/index.tsx": $route_groups_foo_index, - "./routes/signal_shared.tsx": $signal_shared, - "./routes/spoof_state.tsx": $spoof_state, - "./routes/state-in-props/_middleware.ts": $state_in_props_middleware, - "./routes/state-in-props/index.tsx": $state_in_props_index, - "./routes/state-middleware/_middleware.ts": $state_middleware_middleware, - "./routes/state-middleware/foo/_middleware.ts": - $state_middleware_foo_middleware, - "./routes/state-middleware/foo/index.tsx": $state_middleware_foo_index, - "./routes/static.tsx": $static, - "./routes/status_overwrite.tsx": $status_overwrite, - "./routes/std.tsx": $std, - "./routes/umlaut-äöüß.tsx": $umlaut_äöüß, - "./routes/wildcard.tsx": $wildcard, - }, - islands: { - "./islands/Counter.tsx": $Counter, - "./islands/DangerousIsland.tsx": $DangerousIsland, - "./islands/Foo.Bar.tsx": $Foo_Bar, - "./islands/FormIsland.tsx": $FormIsland, - "./islands/Greeter.tsx": $Greeter, - "./islands/HookIsland.tsx": $HookIsland, - "./islands/JsonIsland.tsx": $JsonIsland, - "./islands/MultipleCounters.tsx": $MultipleCounters, - "./islands/ReturningNull.tsx": $ReturningNull, - "./islands/RootFragment.tsx": $RootFragment, - "./islands/RootFragmentWithConditionalFirst.tsx": - $RootFragmentWithConditionalFirst, - "./islands/StringEventIsland.tsx": $StringEventIsland, - "./islands/Test.tsx": $Test, - "./islands/folder/Counter.tsx": $folder_Counter, - "./islands/folder/subfolder/Counter.tsx": $folder_subfolder_Counter, - "./islands/kebab-case-counter-test.tsx": $kebab_case_counter_test, - "./routes/route-groups-islands/(_islands)/Counter.tsx": - $route_groups_islands_islands_Counter, - "./routes/route-groups-islands/(_islands)/invalid.tsx": - $route_groups_islands_islands_invalid, - }, - baseUrl: import.meta.url, -} satisfies Manifest; - -export default manifest; diff --git a/tests/fixture/islands/Counter.test.ts b/tests/fixture/islands/Counter.test.ts deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/tests/fixture/islands/Counter.tsx b/tests/fixture/islands/Counter.tsx deleted file mode 100644 index 5f015b1ee1f..00000000000 --- a/tests/fixture/islands/Counter.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import type { Signal } from "@preact/signals"; -import { IS_BROWSER } from "$fresh/runtime.ts"; - -interface CounterProps { - count: Signal<number>; - id: string; -} - -export default function Counter(props: CounterProps) { - return ( - <div id={props.id}> - <p>{props.count}</p> - <button - id={`b-${props.id}`} - onClick={() => props.count.value += 1} - disabled={!IS_BROWSER} - > - +1 - </button> - </div> - ); -} diff --git a/tests/fixture/islands/DangerousIsland.tsx b/tests/fixture/islands/DangerousIsland.tsx deleted file mode 100644 index 287c481e0a6..00000000000 --- a/tests/fixture/islands/DangerousIsland.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { useEffect, useState } from "preact/hooks"; - -export default function RawIsland(props: { raw: string }) { - const [css, set] = useState(""); - useEffect(() => { - set("raw_ready"); - }, []); - - return <div class={css} dangerouslySetInnerHTML={{ __html: props.raw }} />; -} diff --git a/tests/fixture/islands/Foo.Bar.tsx b/tests/fixture/islands/Foo.Bar.tsx deleted file mode 100644 index 9554001ad5c..00000000000 --- a/tests/fixture/islands/Foo.Bar.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { useEffect, useState } from "preact/hooks"; - -export default function FooBar() { - const [css, setCss] = useState(""); - useEffect(() => { - setCss("ready"); - }, []); - return <h1 class={css}>FooBar island</h1>; -} diff --git a/tests/fixture/islands/FormIsland.tsx b/tests/fixture/islands/FormIsland.tsx deleted file mode 100644 index e9e335cd8ec..00000000000 --- a/tests/fixture/islands/FormIsland.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { useEffect, useRef } from "preact/hooks"; -import { ComponentChildren } from "preact"; - -export function FormIsland({ children }: { children: ComponentChildren }) { - const ref = useRef<HTMLParagraphElement | null>(null); - - useEffect(() => { - if (!ref.current) return; - ref.current.textContent = "Revived: true"; - }, []); - - return ( - <form onSubmit={(e) => e.preventDefault()}> - <p class="form-revived" ref={ref}>Revived: false</p> - {children} - </form> - ); -} diff --git a/tests/fixture/islands/Greeter.tsx b/tests/fixture/islands/Greeter.tsx deleted file mode 100644 index 5f679d94615..00000000000 --- a/tests/fixture/islands/Greeter.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export default function Greeter(props: { site: string }) { - return <h1>{props.site}</h1>; -} diff --git a/tests/fixture/islands/HookIsland.tsx b/tests/fixture/islands/HookIsland.tsx deleted file mode 100644 index 9b59ee19999..00000000000 --- a/tests/fixture/islands/HookIsland.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { useState } from "preact/hooks"; - -export function HookIsland() { - const [v, set] = useState(0); - return ( - <div> - <p>{v}</p> - <button onClick={() => set((v) => v + 1)}>update</button> - </div> - ); -} diff --git a/tests/fixture/islands/JsonIsland.tsx b/tests/fixture/islands/JsonIsland.tsx deleted file mode 100644 index 5feee1c63b9..00000000000 --- a/tests/fixture/islands/JsonIsland.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import json from "./data.json" with { type: "json" }; - -export default function JsonIsland() { - return <pre>{JSON.stringify(json,null, 2)}</pre>; -} diff --git a/tests/fixture/islands/MultipleCounters.tsx b/tests/fixture/islands/MultipleCounters.tsx deleted file mode 100644 index e3f41068e00..00000000000 --- a/tests/fixture/islands/MultipleCounters.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import type { Signal } from "@preact/signals"; -import { IS_BROWSER } from "$fresh/runtime.ts"; - -interface CounterProps { - count: Signal<number>; - id: string; -} - -export const thisShouldNotCauseProblems = 42; - -export default function CounterZero(props: CounterProps) { - return ( - <div id={props.id}> - <p>{props.count}</p> - <button - id={`b-${props.id}`} - onClick={() => props.count.value += 1} - disabled={!IS_BROWSER} - > - +1 - </button> - </div> - ); -} - -export function CounterOne(props: CounterProps) { - return CounterZero(props); -} - -export function CounterTwo(props: CounterProps) { - return CounterZero(props); -} diff --git a/tests/fixture/islands/ReturningNull.tsx b/tests/fixture/islands/ReturningNull.tsx deleted file mode 100644 index 954ab9163a2..00000000000 --- a/tests/fixture/islands/ReturningNull.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { useEffect } from "preact/hooks"; - -export default function ReturningNull() { - useEffect(() => { - const p = document.createElement("p"); - p.textContent = "Hello, null!"; - p.className = "added-by-use-effect"; - - document.body.appendChild(p); - }, []); - - return null; -} diff --git a/tests/fixture/islands/RootFragment.tsx b/tests/fixture/islands/RootFragment.tsx deleted file mode 100644 index 42cad83fcc7..00000000000 --- a/tests/fixture/islands/RootFragment.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { useSignal } from "@preact/signals"; - -export default function RootFragment() { - const shown = useSignal(false); - - return ( - <> - Hello - <div onClick={() => shown.value = true} id="root-fragment-click-me"> - World - </div> - {shown.value && <div>I'm rendered now</div>} - </> - ); -} diff --git a/tests/fixture/islands/RootFragmentWithConditionalFirst.tsx b/tests/fixture/islands/RootFragmentWithConditionalFirst.tsx deleted file mode 100644 index e4cf4156996..00000000000 --- a/tests/fixture/islands/RootFragmentWithConditionalFirst.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { useSignal } from "@preact/signals"; - -export default function RootFragmentWithConditionalFirst() { - const shown = useSignal(false); - - return ( - <> - {shown.value && <div>I'm rendered on top</div>} - Hello - <div - onClick={() => shown.value = true} - id="root-fragment-conditional-first-click-me" - > - World - </div> - </> - ); -} diff --git a/tests/fixture/islands/StringEventIsland.tsx b/tests/fixture/islands/StringEventIsland.tsx deleted file mode 100644 index 7d07d1d1e35..00000000000 --- a/tests/fixture/islands/StringEventIsland.tsx +++ /dev/null @@ -1,9 +0,0 @@ -export default function StringEventIsland() { - return ( - <button // @ts-ignore - we don't officially recommend this, but lots of - // apps pre Fresh 1.2 use string based click handlers. - onClick="document.querySelector('p').textContent = 'it works'"> - click me - </button> - ); -} diff --git a/tests/fixture/islands/Test.tsx b/tests/fixture/islands/Test.tsx deleted file mode 100644 index 131737d8166..00000000000 --- a/tests/fixture/islands/Test.tsx +++ /dev/null @@ -1,13 +0,0 @@ -export default function Test(props: { message: string }) { - return ( - <div> - <p>{props.message}</p> - <img - id="img-in-island" - src="/image.png" - srcset="/image.png 1x" - height={130} - /> - </div> - ); -} diff --git a/tests/fixture/islands/data.json b/tests/fixture/islands/data.json deleted file mode 100644 index e99d055bdc9..00000000000 --- a/tests/fixture/islands/data.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "foo": "it works" -} diff --git a/tests/fixture/islands/folder/Counter.tsx b/tests/fixture/islands/folder/Counter.tsx deleted file mode 100644 index 5f015b1ee1f..00000000000 --- a/tests/fixture/islands/folder/Counter.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import type { Signal } from "@preact/signals"; -import { IS_BROWSER } from "$fresh/runtime.ts"; - -interface CounterProps { - count: Signal<number>; - id: string; -} - -export default function Counter(props: CounterProps) { - return ( - <div id={props.id}> - <p>{props.count}</p> - <button - id={`b-${props.id}`} - onClick={() => props.count.value += 1} - disabled={!IS_BROWSER} - > - +1 - </button> - </div> - ); -} diff --git a/tests/fixture/islands/folder/subfolder/Counter.tsx b/tests/fixture/islands/folder/subfolder/Counter.tsx deleted file mode 100644 index 5f015b1ee1f..00000000000 --- a/tests/fixture/islands/folder/subfolder/Counter.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import type { Signal } from "@preact/signals"; -import { IS_BROWSER } from "$fresh/runtime.ts"; - -interface CounterProps { - count: Signal<number>; - id: string; -} - -export default function Counter(props: CounterProps) { - return ( - <div id={props.id}> - <p>{props.count}</p> - <button - id={`b-${props.id}`} - onClick={() => props.count.value += 1} - disabled={!IS_BROWSER} - > - +1 - </button> - </div> - ); -} diff --git a/tests/fixture/islands/kebab-case-counter-test.tsx b/tests/fixture/islands/kebab-case-counter-test.tsx deleted file mode 100644 index 6ee842d0420..00000000000 --- a/tests/fixture/islands/kebab-case-counter-test.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import type { Signal } from "@preact/signals"; -import { IS_BROWSER } from "$fresh/runtime.ts"; - -interface KebabCaseFileNameTestProps { - count: Signal<number>; - id: string; -} - -export default function KebabCaseFileNameTest( - props: KebabCaseFileNameTestProps, -) { - return ( - <div id={props.id}> - <p>{props.count}</p> - <button - id={`b-${props.id}`} - onClick={() => props.count.value += 1} - disabled={!IS_BROWSER} - > - +1 - </button> - </div> - ); -} diff --git a/tests/fixture/islands/test_test.ts b/tests/fixture/islands/test_test.ts deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/tests/fixture/main.ts b/tests/fixture/main.ts deleted file mode 100644 index fc9359215e3..00000000000 --- a/tests/fixture/main.ts +++ /dev/null @@ -1,11 +0,0 @@ -/// <reference no-default-lib="true" /> -/// <reference lib="dom" /> -/// <reference lib="dom.iterable" /> -/// <reference lib="dom.asynciterable" /> -/// <reference lib="deno.ns" /> - -import { start } from "$fresh/server.ts"; -import manifest from "./fresh.gen.ts"; -import config from "./fresh.config.ts"; - -await start(manifest, config); diff --git a/tests/fixture/main_tls.ts b/tests/fixture/main_tls.ts deleted file mode 100644 index be8ec20b3db..00000000000 --- a/tests/fixture/main_tls.ts +++ /dev/null @@ -1,12 +0,0 @@ -/// <reference no-default-lib="true" /> -/// <reference lib="dom" /> -/// <reference lib="dom.iterable" /> -/// <reference lib="dom.asynciterable" /> -/// <reference lib="deno.ns" /> - -import { start } from "$fresh/server.ts"; -import manifest from "./fresh.gen.ts"; -import config from "./fresh.config.ts"; - -// this just exists to function as a type check to assert that we can actually pass a key and cert in -await start(manifest, { ...config, key: "test", cert: "test" }); diff --git a/tests/fixture/main_wasm.ts b/tests/fixture/main_wasm.ts deleted file mode 100644 index 2068d2dcdf3..00000000000 --- a/tests/fixture/main_wasm.ts +++ /dev/null @@ -1,14 +0,0 @@ -// Simulate Deno Deploy environment - -/// <reference no-default-lib="true" /> -/// <reference lib="dom" /> -/// <reference lib="dom.iterable" /> -/// <reference lib="dom.asynciterable" /> -/// <reference lib="deno.ns" /> - -import "./polyfill_deno_deploy.ts"; -import { start } from "$fresh/server.ts"; -import manifest from "./fresh.gen.ts"; -import config from "./fresh.config.ts"; - -await start(manifest, config); diff --git a/tests/fixture/polyfill_deno_deploy.ts b/tests/fixture/polyfill_deno_deploy.ts deleted file mode 100644 index 73380028741..00000000000 --- a/tests/fixture/polyfill_deno_deploy.ts +++ /dev/null @@ -1,5 +0,0 @@ -// Simulate Deno Deploy environment - -//@ts-ignore: Remove Deno.run for simulating deno deploy env -// deno-lint-ignore no-deprecated-deno-api -delete Deno.run; diff --git a/tests/fixture/routes/404-from-middleware-throw/_middleware.ts b/tests/fixture/routes/404-from-middleware-throw/_middleware.ts deleted file mode 100644 index 4d833981681..00000000000 --- a/tests/fixture/routes/404-from-middleware-throw/_middleware.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { FreshContext } from "$fresh/server.ts"; - -// handlers are supposed to return something, so in order to make type checker on the manifest happy, we'll use any to escape it -export function handler( - _req: Request, - _ctx: FreshContext, - // deno-lint-ignore no-explicit-any -): any { - throw new Deno.errors.NotFound(); -} diff --git a/tests/fixture/routes/404-from-middleware-throw/index.tsx b/tests/fixture/routes/404-from-middleware-throw/index.tsx deleted file mode 100644 index b60a62bc9b0..00000000000 --- a/tests/fixture/routes/404-from-middleware-throw/index.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { defineRoute } from "$fresh/server.ts"; - -export default defineRoute((req, ctx) => { - return "This never gets shown, because the middleware throws an error."; -}); diff --git a/tests/fixture/routes/404-from-middleware/_middleware.ts b/tests/fixture/routes/404-from-middleware/_middleware.ts deleted file mode 100644 index fa06e05b5e3..00000000000 --- a/tests/fixture/routes/404-from-middleware/_middleware.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { FreshContext } from "$fresh/server.ts"; - -export async function handler(_req: Request, ctx: FreshContext) { - return await ctx.renderNotFound(); -} diff --git a/tests/fixture/routes/404-from-middleware/index.tsx b/tests/fixture/routes/404-from-middleware/index.tsx deleted file mode 100644 index 65ce61687e5..00000000000 --- a/tests/fixture/routes/404-from-middleware/index.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { defineRoute } from "$fresh/server.ts"; - -export default defineRoute((req, ctx) => { - return "This never gets shown, because the middleware calls ctx.renderNotFound."; -}); diff --git a/tests/fixture/routes/404_from_throw.tsx b/tests/fixture/routes/404_from_throw.tsx deleted file mode 100644 index a5e7976dc7c..00000000000 --- a/tests/fixture/routes/404_from_throw.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import { Handlers } from "$fresh/server.ts"; - -export const handler: Handlers = { - GET(_req, _ctx) { - throw new Deno.errors.NotFound(); - }, -}; diff --git a/tests/fixture/routes/[name].tsx b/tests/fixture/routes/[name].tsx deleted file mode 100644 index c510be8d631..00000000000 --- a/tests/fixture/routes/[name].tsx +++ /dev/null @@ -1,7 +0,0 @@ -interface Props { - params: Record<string, string | string[]>; -} - -export default function Greet(props: Props) { - return <div>Hello {props.params.name}</div>; -} diff --git a/tests/fixture/routes/_404.tsx b/tests/fixture/routes/_404.tsx deleted file mode 100644 index 3553d4dcfdb..00000000000 --- a/tests/fixture/routes/_404.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { PageProps } from "$fresh/server.ts"; - -type Data = { hello: string }; -type State = { root: string }; - -export default function NotFoundPage( - { data, state, url }: PageProps<Data | undefined, State>, -) { - // Checks that we have the correct type for state - state.root satisfies string; - - return ( - <> - <p>404 not found: {url.pathname}</p> - {data?.hello && <p>Hello {data.hello}</p>} - <p>State root: {state.root}</p> - </> - ); -} diff --git a/tests/fixture/routes/_404_test.tsx b/tests/fixture/routes/_404_test.tsx deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/tests/fixture/routes/_500.tsx b/tests/fixture/routes/_500.tsx deleted file mode 100644 index 544455cc91c..00000000000 --- a/tests/fixture/routes/_500.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { PageProps } from "$fresh/server.ts"; - -export default function Error500Page({ error }: PageProps) { - return <p>500 internal error: {(error as Error).message}</p>; -} diff --git a/tests/fixture/routes/_app.tsx b/tests/fixture/routes/_app.tsx deleted file mode 100644 index cf48c3927f0..00000000000 --- a/tests/fixture/routes/_app.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { Head } from "$fresh/runtime.ts"; -import { PageProps } from "$fresh/server.ts"; - -export type TestState = { - root: string; - stateInProps: string; -}; - -export default function App(props: PageProps<unknown, TestState>) { - const statefulValue = props.state?.root === "root_mw" - ? "The freshest framework!" - : ""; - const specialCase = props.state?.stateInProps; - return ( - <> - <Head> - <meta name="description" content="Hello world!" /> - <meta name="generator" content={statefulValue} /> - {specialCase && <meta name="specialTag" content={specialCase} />} - </Head> - <props.Component /> - </> - ); -} diff --git a/tests/fixture/routes/_middleware.ts b/tests/fixture/routes/_middleware.ts deleted file mode 100644 index bb417a524ba..00000000000 --- a/tests/fixture/routes/_middleware.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { FreshContext, MiddlewareHandler } from "$fresh/server.ts"; - -// cors middleware -async function corsHandler(req: Request, ctx: FreshContext) { - if (req.method == "OPTIONS") { - return new Response(null, { - status: 204, - }); - } - const origin = req.headers.get("Origin") || "*"; - const resp = await ctx.next(); - const headers = resp.headers; - - headers.set("Access-Control-Allow-Origin", origin); - headers.set("Access-Control-Allow-Credentials", "true"); - headers.set( - "Access-Control-Allow-Headers", - "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With", - ); - headers.set( - "Access-Control-Allow-Methods", - "POST, OPTIONS, GET, PUT, DELETE", - ); - - return resp; -} - -// log middleware -async function logHandler(_req: Request, ctx: FreshContext) { - const since = new Date(); - const resp = await ctx.next(); - const latency = (+new Date()) - (+since); - resp.headers.set("latency", `${latency}`); - return resp; -} - -async function rootHandler(_req: Request, ctx: FreshContext) { - ctx.state.root = "root_mw"; - const resp = await ctx.next(); - resp.headers.set("server", "fresh test server"); - return resp; -} - -async function kindHandler(_req: Request, ctx: FreshContext) { - const resp = await ctx.next(); - resp.headers.set("destination", ctx.destination); - return resp; -} - -export const handler: MiddlewareHandler | MiddlewareHandler[] = [ - rootHandler, - logHandler, - kindHandler, - corsHandler, -]; diff --git a/tests/fixture/routes/admin/[site].tsx b/tests/fixture/routes/admin/[site].tsx deleted file mode 100644 index 5faa569e682..00000000000 --- a/tests/fixture/routes/admin/[site].tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { Handler, PageProps } from "$fresh/server.ts"; -import Greeter from "../../islands/Greeter.tsx"; - -export const handler: Handler = (_req, ctx) => - ctx.render({ site: ctx.params.site }); - -export default function Component(props: PageProps<{ site: string }>) { - return <Greeter site={props.data?.site ?? "not working"} />; -} diff --git a/tests/fixture/routes/api/get_only.ts b/tests/fixture/routes/api/get_only.ts deleted file mode 100644 index 6e5411566fa..00000000000 --- a/tests/fixture/routes/api/get_only.ts +++ /dev/null @@ -1,12 +0,0 @@ -export const handler = { - GET() { - return new Response("Get fresh!", { - headers: { - "Content-Type": "application/json; charset=utf-8", - }, - }); - }, - NOTAMETHOD() { - throw new Error("unreachable"); - }, -}; diff --git a/tests/fixture/routes/api/head_override.ts b/tests/fixture/routes/api/head_override.ts deleted file mode 100644 index b317194d3d5..00000000000 --- a/tests/fixture/routes/api/head_override.ts +++ /dev/null @@ -1,17 +0,0 @@ -export const handler = { - HEAD() { - return new Response(null, { - status: 204, - headers: { - "Content-Type": "text/html; charset=utf-8", - }, - }); - }, - GET() { - return new Response("Get fresh!", { - headers: { - "Content-Type": "application/json; charset=utf-8", - }, - }); - }, -}; diff --git a/tests/fixture/routes/assetsCaching/index.tsx b/tests/fixture/routes/assetsCaching/index.tsx deleted file mode 100644 index a16fdc7bdeb..00000000000 --- a/tests/fixture/routes/assetsCaching/index.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import Test from "../../islands/Test.tsx"; - -export default function Home() { - return ( - <div> - <div style={{ marginTop: 20 }}>img-with-hashing</div> - <img id="img-with-hashing" src="/image.png" height={130} /> - - <div style={{ marginTop: 20 }}>img-without-hashing</div> - <img - id="img-without-hashing" - src="/image.png" - data-fresh-disable-lock - height={130} - /> - - <Test message="In island" /> - - <div style={{ marginTop: 20 }}>img-external</div> - <img - id="img-missing" - src="https://fresh.deno.dev/favicon.ico" - height={130} - /> - </div> - ); -} diff --git a/tests/fixture/routes/books/[id].tsx b/tests/fixture/routes/books/[id].tsx deleted file mode 100644 index 554885569cf..00000000000 --- a/tests/fixture/routes/books/[id].tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { PageProps, RouteConfig } from "$fresh/server.ts"; - -export default function Page(props: PageProps) { - return <div>Book {props.params.id}</div>; -} - -export const config: RouteConfig = { - routeOverride: "/books/:id(\\d+)", -}; diff --git a/tests/fixture/routes/connInfo.ts b/tests/fixture/routes/connInfo.ts deleted file mode 100644 index f493400c112..00000000000 --- a/tests/fixture/routes/connInfo.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Handlers } from "../../../server.ts"; - -export const handler: Handlers = { - GET(_req, ctx) { - return new Response((ctx.remoteAddr as Deno.NetAddr).hostname); - }, -}; diff --git a/tests/fixture/routes/ctx_config.tsx b/tests/fixture/routes/ctx_config.tsx deleted file mode 100644 index 0a570136126..00000000000 --- a/tests/fixture/routes/ctx_config.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { defineRoute } from "$fresh/server.ts"; -import { relative, SEPARATOR } from "$fresh/tests/deps.ts"; - -export default defineRoute((_req, ctx) => { - const value = JSON.stringify(ctx, (key, value) => { - if (key === "outDir" || key == "staticDir") { - return relative(Deno.cwd(), value).split(SEPARATOR).join("/"); - } - if (typeof value === "function") return value.constructor.name; - if (value === undefined) return "<undefined>"; - return value; - }, 2); - - return ( - <pre> - {value} - </pre> - ); -}); diff --git a/tests/fixture/routes/ctx_config_props.tsx b/tests/fixture/routes/ctx_config_props.tsx deleted file mode 100644 index e2b7c04f852..00000000000 --- a/tests/fixture/routes/ctx_config_props.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { PageProps } from "$fresh/server.ts"; -import { relative, SEPARATOR } from "../../deps.ts"; - -export default function Page(props: PageProps) { - const value = JSON.stringify(props, (key, value) => { - if (key === "outDir" || key == "staticDir") { - return relative(Deno.cwd(), value).split(SEPARATOR).join("/"); - } - if (typeof value === "function") return value.constructor.name; - if (value === undefined) return "<undefined>"; - return value; - }, 2); - - return <pre>{value}</pre>; -} diff --git a/tests/fixture/routes/error_boundary.tsx b/tests/fixture/routes/error_boundary.tsx deleted file mode 100644 index 14209a80b64..00000000000 --- a/tests/fixture/routes/error_boundary.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { Component } from "preact"; - -class ErrorBoundary extends Component { - state = { error: null } as { error: Error | null }; - - static getDerivedStateFromError(error: Error) { - return { error }; - } - - render() { - return this.state.error - ? <p>{this.state.error.message}</p> - : <>{this.props.children}</>; - } -} - -function Thrower(): preact.JSX.Element { - throw new Error("it works"); -} - -export default function ErrorBoundaryPage() { - return ( - <ErrorBoundary> - <Thrower /> - </ErrorBoundary> - ); -} diff --git a/tests/fixture/routes/event_handler_string.tsx b/tests/fixture/routes/event_handler_string.tsx deleted file mode 100644 index 4adab80b960..00000000000 --- a/tests/fixture/routes/event_handler_string.tsx +++ /dev/null @@ -1,12 +0,0 @@ -export default function Page() { - return ( - <div> - <p>it doesn't work</p> - <button // @ts-ignore - we don't officially recommend this, but lots of - // apps pre Fresh 1.2 use string based click handlers. - onClick="document.querySelector('p').textContent = 'it works'"> - click me - </button> - </div> - ); -} diff --git a/tests/fixture/routes/event_handler_string_island.tsx b/tests/fixture/routes/event_handler_string_island.tsx deleted file mode 100644 index ed73e35670c..00000000000 --- a/tests/fixture/routes/event_handler_string_island.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import StringEventIsland from "$fresh/tests/fixture/islands/StringEventIsland.tsx"; - -export default function Page() { - return ( - <div> - <p>it doesn't work</p> - <StringEventIsland /> - </div> - ); -} diff --git a/tests/fixture/routes/evil.tsx b/tests/fixture/routes/evil.tsx deleted file mode 100644 index afa86fa7ab6..00000000000 --- a/tests/fixture/routes/evil.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import Test from "../islands/Test.tsx"; - -export default function EvilPage() { - return ( - <div> - <Test message={`</script><script>alert('test')</script>`} /> - </div> - ); -} diff --git a/tests/fixture/routes/failure.ts b/tests/fixture/routes/failure.ts deleted file mode 100644 index 4451c47ee95..00000000000 --- a/tests/fixture/routes/failure.ts +++ /dev/null @@ -1,5 +0,0 @@ -export const handler = { - GET() { - throw Error("it errored!"); - }, -}; diff --git a/tests/fixture/routes/foo.bar.baz.tsx b/tests/fixture/routes/foo.bar.baz.tsx deleted file mode 100644 index 1d040076877..00000000000 --- a/tests/fixture/routes/foo.bar.baz.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export default function Page() { - return <p>it works</p>; -} diff --git a/tests/fixture/routes/foo.bar.tsx b/tests/fixture/routes/foo.bar.tsx deleted file mode 100644 index 93427a1fe11..00000000000 --- a/tests/fixture/routes/foo.bar.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import FooBar from "../islands/Foo.Bar.tsx"; - -export default function Page() { - return <FooBar />; -} diff --git a/tests/fixture/routes/head_deduplicate.tsx b/tests/fixture/routes/head_deduplicate.tsx deleted file mode 100644 index 406f3b2a3fb..00000000000 --- a/tests/fixture/routes/head_deduplicate.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { Head } from "$fresh/runtime.ts"; - -export default function Page() { - return ( - <> - <h1>Card head css deduplication</h1> - <Head> - <title>foo - - - - - bar - - - - - ); -} diff --git a/tests/fixture/routes/hooks-server/island.tsx b/tests/fixture/routes/hooks-server/island.tsx deleted file mode 100644 index a814fa5ce73..00000000000 --- a/tests/fixture/routes/hooks-server/island.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { HookIsland } from "../../islands/HookIsland.tsx"; - -export default function Page() { - return ; -} diff --git a/tests/fixture/routes/hooks-server/useReducer.tsx b/tests/fixture/routes/hooks-server/useReducer.tsx deleted file mode 100644 index cb4ef079376..00000000000 --- a/tests/fixture/routes/hooks-server/useReducer.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import { useReducer } from "preact/hooks"; - -export default function Page() { - useReducer(() => {}, undefined); - return

useReducer

; -} diff --git a/tests/fixture/routes/hooks-server/useState.tsx b/tests/fixture/routes/hooks-server/useState.tsx deleted file mode 100644 index f33ca543d2d..00000000000 --- a/tests/fixture/routes/hooks-server/useState.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import { useState } from "preact/hooks"; - -export default function Page() { - useState(); - return

useState

; -} diff --git a/tests/fixture/routes/i18n/[[lang]]/lang.tsx b/tests/fixture/routes/i18n/[[lang]]/lang.tsx deleted file mode 100644 index 31602290cf2..00000000000 --- a/tests/fixture/routes/i18n/[[lang]]/lang.tsx +++ /dev/null @@ -1,9 +0,0 @@ -interface Props { - params: Record; -} - -export default function Lang(props: Props) { - return props.params.lang - ?
Hello {props.params.lang}
- :
Hello
; -} diff --git a/tests/fixture/routes/index.tsx b/tests/fixture/routes/index.tsx deleted file mode 100644 index e182400072f..00000000000 --- a/tests/fixture/routes/index.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { IS_BROWSER } from "$fresh/runtime.ts"; -import Test from "../islands/Test.tsx"; - -export default function Home() { - return ( -
- -

{IS_BROWSER ? "Viewing browser render." : "Viewing JIT render."}

-
- ); -} diff --git a/tests/fixture/routes/intercept.tsx b/tests/fixture/routes/intercept.tsx deleted file mode 100644 index 956fbe24d5d..00000000000 --- a/tests/fixture/routes/intercept.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { FreshContext } from "$fresh/server.ts"; - -export default function Page() { - return
This is HTML
; -} - -export const handler = { - GET(req: Request, { render }: FreshContext) { - if (req.headers.get("accept")?.includes("text/html")) { - return render(); - } else { - return new Response("This is plain text"); - } - }, - POST() { - return new Response("POST response"); - }, -}; diff --git a/tests/fixture/routes/intercept_args.tsx b/tests/fixture/routes/intercept_args.tsx deleted file mode 100644 index 64c2781f5b1..00000000000 --- a/tests/fixture/routes/intercept_args.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { Handlers, PageProps } from "$fresh/server.ts"; - -interface Data extends Record { - info: string; -} - -export default function Page({ data }: PageProps) { - return
{data.info}
; -} - -export const handler: Handlers = { - GET(req, { render }) { - if (req.headers.get("accept")?.includes("text/html")) { - return render({ - info: "intercepted", - }); - } else { - return new Response("This is plain text"); - } - }, - POST() { - return new Response("POST response"); - }, -}; diff --git a/tests/fixture/routes/island_json.tsx b/tests/fixture/routes/island_json.tsx deleted file mode 100644 index 4aee517ab9e..00000000000 --- a/tests/fixture/routes/island_json.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import JsonIsland from "../islands/JsonIsland.tsx"; - -export default function Page() { - return ; -} diff --git a/tests/fixture/routes/islands/index.tsx b/tests/fixture/routes/islands/index.tsx deleted file mode 100644 index daffb57622d..00000000000 --- a/tests/fixture/routes/islands/index.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { useSignal } from "@preact/signals"; -import Counter from "../../islands/Counter.tsx"; -import FolderCounter from "../../islands/folder/Counter.tsx"; -import SubfolderCounter from "../../islands/folder/subfolder/Counter.tsx"; -import KebabCaseFileNameTest from "../../islands/kebab-case-counter-test.tsx"; -import Test from "../../islands/Test.tsx"; - -export default function Home() { - return ( -
- - - - - - - `} /> -
- ); -} diff --git a/tests/fixture/routes/islands/multiple_island_exports.tsx b/tests/fixture/routes/islands/multiple_island_exports.tsx deleted file mode 100644 index 0a721065dd0..00000000000 --- a/tests/fixture/routes/islands/multiple_island_exports.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { useSignal } from "@preact/signals"; -import CounterZero from "../../islands/MultipleCounters.tsx"; -import { CounterOne, CounterTwo } from "../../islands/MultipleCounters.tsx"; -import SubfolderCounter from "../../islands/folder/subfolder/Counter.tsx"; - -export default function Home() { - return ( -
- - - - -
- ); -} diff --git a/tests/fixture/routes/islands/returning_null.tsx b/tests/fixture/routes/islands/returning_null.tsx deleted file mode 100644 index 3756e01a700..00000000000 --- a/tests/fixture/routes/islands/returning_null.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import ReturningNull from "$fresh/tests/fixture/islands/ReturningNull.tsx"; - -export default function Home() { - return ; -} diff --git a/tests/fixture/routes/islands/root_fragment.tsx b/tests/fixture/routes/islands/root_fragment.tsx deleted file mode 100644 index a612a624089..00000000000 --- a/tests/fixture/routes/islands/root_fragment.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import RootFragment from "../../islands/RootFragment.tsx"; - -export default function Home() { - return ( -
- -
- ); -} diff --git a/tests/fixture/routes/islands/root_fragment_conditional_first.tsx b/tests/fixture/routes/islands/root_fragment_conditional_first.tsx deleted file mode 100644 index 49a517a6750..00000000000 --- a/tests/fixture/routes/islands/root_fragment_conditional_first.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import RootFragmentWithConditionalFirst from "../../islands/RootFragmentWithConditionalFirst.tsx"; - -export default function Home() { - return ( -
- -
- ); -} diff --git a/tests/fixture/routes/layeredMdw/_middleware.ts b/tests/fixture/routes/layeredMdw/_middleware.ts deleted file mode 100644 index 7831c5d6677..00000000000 --- a/tests/fixture/routes/layeredMdw/_middleware.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { FreshContext } from "$fresh/server.ts"; - -export async function handler(_req: Request, ctx: FreshContext) { - ctx.state.layer1 = "layer1_mw"; - const resp = await ctx.next(); - resp.headers.set("server", "fresh test server layer1"); - return resp; -} diff --git a/tests/fixture/routes/layeredMdw/layer2-no-mw/without_mw.ts b/tests/fixture/routes/layeredMdw/layer2-no-mw/without_mw.ts deleted file mode 100644 index 650e9be8897..00000000000 --- a/tests/fixture/routes/layeredMdw/layer2-no-mw/without_mw.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Handlers } from "../../../../../server.ts"; - -interface State { - root: string; - layer1: string; - layer3: string; -} - -export const handler: Handlers = { - GET(_req: Request, { state }) { - return new Response(JSON.stringify(state)); - }, -}; diff --git a/tests/fixture/routes/layeredMdw/layer2-with-params/[tenantId]/[id].ts b/tests/fixture/routes/layeredMdw/layer2-with-params/[tenantId]/[id].ts deleted file mode 100644 index d9b353dc405..00000000000 --- a/tests/fixture/routes/layeredMdw/layer2-with-params/[tenantId]/[id].ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Handlers } from "$fresh/server.ts"; - -export const handler: Handlers = { - GET(_req: Request, _ctx) { - return new Response(JSON.stringify({})); - }, -}; diff --git a/tests/fixture/routes/layeredMdw/layer2-with-params/[tenantId]/_middleware.ts b/tests/fixture/routes/layeredMdw/layer2-with-params/[tenantId]/_middleware.ts deleted file mode 100644 index 7590bdef06a..00000000000 --- a/tests/fixture/routes/layeredMdw/layer2-with-params/[tenantId]/_middleware.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { FreshContext } from "$fresh/server.ts"; - -export async function handler(_req: Request, ctx: FreshContext) { - const resp = await ctx.next(); - resp.headers.set("middlewareParams_inner", JSON.stringify(ctx.params)); - return resp; -} diff --git a/tests/fixture/routes/layeredMdw/layer2-with-params/_middleware.ts b/tests/fixture/routes/layeredMdw/layer2-with-params/_middleware.ts deleted file mode 100644 index 586e8087497..00000000000 --- a/tests/fixture/routes/layeredMdw/layer2-with-params/_middleware.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { FreshContext } from "$fresh/server.ts"; - -export async function handler(_req: Request, ctx: FreshContext) { - const resp = await ctx.next(); - resp.headers.set("middlewareParams_outer", JSON.stringify(ctx.params)); - return resp; -} diff --git a/tests/fixture/routes/layeredMdw/layer2/_middleware.ts b/tests/fixture/routes/layeredMdw/layer2/_middleware.ts deleted file mode 100644 index 8cdaa53b58a..00000000000 --- a/tests/fixture/routes/layeredMdw/layer2/_middleware.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { FreshContext } from "$fresh/server.ts"; - -interface State { - root: string; - layer1: string; - layer2: string; -} - -export async function handler( - _req: Request, - ctx: FreshContext, -) { - ctx.state.layer2 = "layer2_mw"; - const resp = await ctx.next(); - return resp; -} diff --git a/tests/fixture/routes/layeredMdw/layer2/abc.ts b/tests/fixture/routes/layeredMdw/layer2/abc.ts deleted file mode 100644 index 2be04d0689a..00000000000 --- a/tests/fixture/routes/layeredMdw/layer2/abc.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Handlers } from "../../../../../server.ts"; - -interface State { - root: string; - layer1: string; - layer2: string; -} - -export const handler: Handlers = { - GET(_req: Request, { state }) { - return new Response(JSON.stringify(state)); - }, -}; diff --git a/tests/fixture/routes/layeredMdw/layer2/index.ts b/tests/fixture/routes/layeredMdw/layer2/index.ts deleted file mode 100644 index 2be04d0689a..00000000000 --- a/tests/fixture/routes/layeredMdw/layer2/index.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Handlers } from "../../../../../server.ts"; - -interface State { - root: string; - layer1: string; - layer2: string; -} - -export const handler: Handlers = { - GET(_req: Request, { state }) { - return new Response(JSON.stringify(state)); - }, -}; diff --git a/tests/fixture/routes/layeredMdw/layer2/layer3/[id].ts b/tests/fixture/routes/layeredMdw/layer2/layer3/[id].ts deleted file mode 100644 index 986a7ca3078..00000000000 --- a/tests/fixture/routes/layeredMdw/layer2/layer3/[id].ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Handlers } from "../../../../../../server.ts"; - -interface State { - root: string; - layer1: string; - layer2: string; - layer3: string; -} - -export const handler: Handlers = { - GET(_req: Request, { state }) { - return new Response(JSON.stringify(state)); - }, -}; diff --git a/tests/fixture/routes/layeredMdw/layer2/layer3/_middleware.ts b/tests/fixture/routes/layeredMdw/layer2/layer3/_middleware.ts deleted file mode 100644 index 4b025951557..00000000000 --- a/tests/fixture/routes/layeredMdw/layer2/layer3/_middleware.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { FreshContext } from "$fresh/server.ts"; - -export async function handler(_req: Request, ctx: FreshContext) { - ctx.state.layer3 = "layer3_mw"; - const resp = await ctx.next(); - resp.headers.set("server", "fresh test server layer3"); - resp.headers.set("layer3", "fresh test server layer3"); - return resp; -} diff --git a/tests/fixture/routes/layeredMdw/nesting/[tenant]/[environment]/[id].tsx b/tests/fixture/routes/layeredMdw/nesting/[tenant]/[environment]/[id].tsx deleted file mode 100644 index b20b557dc0f..00000000000 --- a/tests/fixture/routes/layeredMdw/nesting/[tenant]/[environment]/[id].tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { Handlers, PageProps } from "$fresh/server.ts"; - -export const handler: Handlers = { - async GET(_req: Request, ctx) { - const order = ctx.state.middlewareNestingOrder as string; - const resp = await ctx.render(order + "4"); - return resp; - }, -}; - -export default function Page(props: PageProps) { - return
{props.data}
; -} diff --git a/tests/fixture/routes/layeredMdw/nesting/[tenant]/[environment]/_middleware.ts b/tests/fixture/routes/layeredMdw/nesting/[tenant]/[environment]/_middleware.ts deleted file mode 100644 index 344a21836d5..00000000000 --- a/tests/fixture/routes/layeredMdw/nesting/[tenant]/[environment]/_middleware.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { FreshContext } from "$fresh/server.ts"; - -export async function handler(_req: Request, ctx: FreshContext) { - ctx.state.middlewareNestingOrder += "3"; - const resp = await ctx.next(); - return resp; -} diff --git a/tests/fixture/routes/layeredMdw/nesting/[tenant]/_middleware.ts b/tests/fixture/routes/layeredMdw/nesting/[tenant]/_middleware.ts deleted file mode 100644 index f567e137891..00000000000 --- a/tests/fixture/routes/layeredMdw/nesting/[tenant]/_middleware.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { FreshContext } from "$fresh/server.ts"; - -export async function handler(_req: Request, ctx: FreshContext) { - ctx.state.middlewareNestingOrder += "2"; - const resp = await ctx.next(); - return resp; -} diff --git a/tests/fixture/routes/layeredMdw/nesting/_middleware.ts b/tests/fixture/routes/layeredMdw/nesting/_middleware.ts deleted file mode 100644 index 7fea5aaae2c..00000000000 --- a/tests/fixture/routes/layeredMdw/nesting/_middleware.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { FreshContext } from "$fresh/server.ts"; - -export async function handler(_req: Request, ctx: FreshContext) { - ctx.state.middlewareNestingOrder = "1"; - const resp = await ctx.next(); - return resp; -} diff --git a/tests/fixture/routes/middleware-error-handler/_middleware.ts b/tests/fixture/routes/middleware-error-handler/_middleware.ts deleted file mode 100644 index 805b93f762b..00000000000 --- a/tests/fixture/routes/middleware-error-handler/_middleware.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { FreshContext } from "$fresh/server.ts"; - -export async function handler( - _req: Request, - ctx: FreshContext, -) { - try { - ctx.state.flag = true; - return await ctx.next(); - } catch (error) { - console.log("we're very thoroughly dealing with this error here: " + error); - throw Error("don't show the full error for security purposes"); - } -} diff --git a/tests/fixture/routes/middleware-error-handler/index.tsx b/tests/fixture/routes/middleware-error-handler/index.tsx deleted file mode 100644 index 887b61ef34c..00000000000 --- a/tests/fixture/routes/middleware-error-handler/index.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { Handlers, PageProps } from "$fresh/server.ts"; - -export const handler: Handlers = { - GET(req, ctx) { - return ctx.render(ctx.state.flag); - }, -}; - -export default function Home(props: PageProps) { - if (props.data) { - throw Error("i'm erroring on purpose"); - } - return
this won't get shown
; -} diff --git a/tests/fixture/routes/middleware_root.ts b/tests/fixture/routes/middleware_root.ts deleted file mode 100644 index 78340640d5e..00000000000 --- a/tests/fixture/routes/middleware_root.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Handlers } from "../../../server.ts"; - -interface State { - root: string; -} - -export const handler: Handlers = { - GET(_req: Request, { state }) { - return new Response(JSON.stringify(state)); - }, -}; diff --git a/tests/fixture/routes/movies/[foo].json.ts b/tests/fixture/routes/movies/[foo].json.ts deleted file mode 100644 index 42171725027..00000000000 --- a/tests/fixture/routes/movies/[foo].json.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { Handler } from "$fresh/server.ts"; - -export const handler: Handler = () => new Response("it works"); diff --git a/tests/fixture/routes/movies/[foo]@[bar].ts b/tests/fixture/routes/movies/[foo]@[bar].ts deleted file mode 100644 index 42171725027..00000000000 --- a/tests/fixture/routes/movies/[foo]@[bar].ts +++ /dev/null @@ -1,3 +0,0 @@ -import { Handler } from "$fresh/server.ts"; - -export const handler: Handler = () => new Response("it works"); diff --git a/tests/fixture/routes/nonce_inline.tsx b/tests/fixture/routes/nonce_inline.tsx deleted file mode 100644 index 6bee8c8ea4d..00000000000 --- a/tests/fixture/routes/nonce_inline.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { useSignal } from "@preact/signals"; -import Counter from "../islands/Counter.tsx"; - -export default function Page() { - const sig = useSignal(0); - return ( -
- - - - - - MDN - -

css style

-
- - ); -} diff --git a/tests/fixture_base_path/routes/index.tsx b/tests/fixture_base_path/routes/index.tsx deleted file mode 100644 index ba73acc0eec..00000000000 --- a/tests/fixture_base_path/routes/index.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { defineRoute } from "$fresh/server.ts"; - -export default defineRoute<{ data: string }>((req, ctx) => { - return ( - <> -

it works

- {ctx.state.data} - - ); -}); diff --git a/tests/fixture_base_path/routes/islands.tsx b/tests/fixture_base_path/routes/islands.tsx deleted file mode 100644 index b55c5f7019c..00000000000 --- a/tests/fixture_base_path/routes/islands.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { useSignal } from "@preact/signals"; -import Counter from "../islands/Counter.tsx"; - -export default function Home() { - return ( -
- -
- ); -} diff --git a/tests/fixture_base_path/static/img.png b/tests/fixture_base_path/static/img.png deleted file mode 100644 index 739cc935325..00000000000 Binary files a/tests/fixture_base_path/static/img.png and /dev/null differ diff --git a/tests/fixture_base_path/static/script.js b/tests/fixture_base_path/static/script.js deleted file mode 100644 index c5ee48e913e..00000000000 --- a/tests/fixture_base_path/static/script.js +++ /dev/null @@ -1 +0,0 @@ -document.querySelector("#script-output").textContent = "it works"; diff --git a/tests/fixture_base_path/static/style.css b/tests/fixture_base_path/static/style.css deleted file mode 100644 index a15c877ac01..00000000000 --- a/tests/fixture_base_path/static/style.css +++ /dev/null @@ -1,3 +0,0 @@ -.foo { - color: red; -} diff --git a/tests/fixture_base_path/static/styles.css b/tests/fixture_base_path/static/styles.css deleted file mode 100644 index b5c61c95671..00000000000 --- a/tests/fixture_base_path/static/styles.css +++ /dev/null @@ -1,3 +0,0 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; diff --git a/tests/fixture_base_path/tailwind.config.ts b/tests/fixture_base_path/tailwind.config.ts deleted file mode 100644 index 63acf078c6e..00000000000 --- a/tests/fixture_base_path/tailwind.config.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Config } from "tailwindcss"; - -export default { - content: [ - "{routes,islands,components}/**/*.{ts,tsx}", - ], -} as Config; diff --git a/tests/fixture_base_path_build/deno.json b/tests/fixture_base_path_build/deno.json deleted file mode 100644 index 6d4529cc180..00000000000 --- a/tests/fixture_base_path_build/deno.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "lock": false, - "imports": { - "$fresh/": "../../", - "preact": "https://esm.sh/preact@10.22.0", - "preact/": "https://esm.sh/preact@10.22.0/", - "tailwindcss": "npm:tailwindcss@3.4.1" - }, - "compilerOptions": { - "jsx": "react-jsx", - "jsxImportSource": "preact" - } -} diff --git a/tests/fixture_base_path_build/dev.ts b/tests/fixture_base_path_build/dev.ts deleted file mode 100755 index 1fe3e340282..00000000000 --- a/tests/fixture_base_path_build/dev.ts +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env -S deno run -A --watch=static/,routes/ - -import dev from "$fresh/dev.ts"; -import config from "./fresh.config.ts"; - -await dev(import.meta.url, "./main.ts", config); diff --git a/tests/fixture_base_path_build/fresh.config.ts b/tests/fixture_base_path_build/fresh.config.ts deleted file mode 100644 index 8668bf3c5e6..00000000000 --- a/tests/fixture_base_path_build/fresh.config.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { defineConfig } from "$fresh/server.ts"; -import tailwind from "$fresh/plugins/tailwind.ts"; - -export default defineConfig({ - router: { basePath: "/foo/bar" }, - plugins: [ - tailwind(), - ], -}); diff --git a/tests/fixture_base_path_build/fresh.gen.ts b/tests/fixture_base_path_build/fresh.gen.ts deleted file mode 100644 index 9c108e56c68..00000000000 --- a/tests/fixture_base_path_build/fresh.gen.ts +++ /dev/null @@ -1,19 +0,0 @@ -// DO NOT EDIT. This file is generated by Fresh. -// This file SHOULD be checked into source version control. -// This file is automatically updated during development when running `dev.ts`. - -import * as $_app from "./routes/_app.tsx"; -import * as $index from "./routes/index.tsx"; - -import type { Manifest } from "$fresh/server.ts"; - -const manifest = { - routes: { - "./routes/_app.tsx": $_app, - "./routes/index.tsx": $index, - }, - islands: {}, - baseUrl: import.meta.url, -} satisfies Manifest; - -export default manifest; diff --git a/tests/fixture_base_path_build/main.ts b/tests/fixture_base_path_build/main.ts deleted file mode 100644 index fc9359215e3..00000000000 --- a/tests/fixture_base_path_build/main.ts +++ /dev/null @@ -1,11 +0,0 @@ -/// -/// -/// -/// -/// - -import { start } from "$fresh/server.ts"; -import manifest from "./fresh.gen.ts"; -import config from "./fresh.config.ts"; - -await start(manifest, config); diff --git a/tests/fixture_base_path_build/routes/_app.tsx b/tests/fixture_base_path_build/routes/_app.tsx deleted file mode 100644 index 2e225e497f4..00000000000 --- a/tests/fixture_base_path_build/routes/_app.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { PageProps } from "$fresh/server.ts"; - -export default function App({ Component }: PageProps) { - return ( - - - - - My Fresh app - - - - - - - ); -} diff --git a/tests/fixture_base_path_build/routes/index.tsx b/tests/fixture_base_path_build/routes/index.tsx deleted file mode 100644 index ee3f8f3c9bd..00000000000 --- a/tests/fixture_base_path_build/routes/index.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export default function Page() { - return

foo

; -} diff --git a/tests/fixture_base_path_build/static/styles.css b/tests/fixture_base_path_build/static/styles.css deleted file mode 100644 index b5c61c95671..00000000000 --- a/tests/fixture_base_path_build/static/styles.css +++ /dev/null @@ -1,3 +0,0 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; diff --git a/tests/fixture_base_path_build/tailwind.config.ts b/tests/fixture_base_path_build/tailwind.config.ts deleted file mode 100644 index 63acf078c6e..00000000000 --- a/tests/fixture_base_path_build/tailwind.config.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Config } from "tailwindcss"; - -export default { - content: [ - "{routes,islands,components}/**/*.{ts,tsx}", - ], -} as Config; diff --git a/tests/fixture_base_path_config/deno.json b/tests/fixture_base_path_config/deno.json deleted file mode 100644 index 6d4529cc180..00000000000 --- a/tests/fixture_base_path_config/deno.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "lock": false, - "imports": { - "$fresh/": "../../", - "preact": "https://esm.sh/preact@10.22.0", - "preact/": "https://esm.sh/preact@10.22.0/", - "tailwindcss": "npm:tailwindcss@3.4.1" - }, - "compilerOptions": { - "jsx": "react-jsx", - "jsxImportSource": "preact" - } -} diff --git a/tests/fixture_base_path_config/dev.ts b/tests/fixture_base_path_config/dev.ts deleted file mode 100755 index 1fe3e340282..00000000000 --- a/tests/fixture_base_path_config/dev.ts +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env -S deno run -A --watch=static/,routes/ - -import dev from "$fresh/dev.ts"; -import config from "./fresh.config.ts"; - -await dev(import.meta.url, "./main.ts", config); diff --git a/tests/fixture_base_path_config/foo/Foo.tsx b/tests/fixture_base_path_config/foo/Foo.tsx deleted file mode 100644 index 965dddb6b17..00000000000 --- a/tests/fixture_base_path_config/foo/Foo.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export function Foo() { - return

foo

; -} diff --git a/tests/fixture_base_path_config/fresh.config.ts b/tests/fixture_base_path_config/fresh.config.ts deleted file mode 100644 index 8668bf3c5e6..00000000000 --- a/tests/fixture_base_path_config/fresh.config.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { defineConfig } from "$fresh/server.ts"; -import tailwind from "$fresh/plugins/tailwind.ts"; - -export default defineConfig({ - router: { basePath: "/foo/bar" }, - plugins: [ - tailwind(), - ], -}); diff --git a/tests/fixture_base_path_config/fresh.gen.ts b/tests/fixture_base_path_config/fresh.gen.ts deleted file mode 100644 index e6643ad216e..00000000000 --- a/tests/fixture_base_path_config/fresh.gen.ts +++ /dev/null @@ -1,19 +0,0 @@ -// DO NOT EDIT. This file is generated by Fresh. -// This file SHOULD be checked into source version control. -// This file is automatically updated during development when running `dev.ts`. - -import * as $_app from "./routes/_app.tsx"; -import * as $index from "./routes/index.tsx"; - -import { type Manifest } from "$fresh/server.ts"; - -const manifest = { - routes: { - "./routes/_app.tsx": $_app, - "./routes/index.tsx": $index, - }, - islands: {}, - baseUrl: import.meta.url, -} satisfies Manifest; - -export default manifest; diff --git a/tests/fixture_base_path_config/main.ts b/tests/fixture_base_path_config/main.ts deleted file mode 100644 index fc9359215e3..00000000000 --- a/tests/fixture_base_path_config/main.ts +++ /dev/null @@ -1,11 +0,0 @@ -/// -/// -/// -/// -/// - -import { start } from "$fresh/server.ts"; -import manifest from "./fresh.gen.ts"; -import config from "./fresh.config.ts"; - -await start(manifest, config); diff --git a/tests/fixture_base_path_config/routes/_app.tsx b/tests/fixture_base_path_config/routes/_app.tsx deleted file mode 100644 index 2e225e497f4..00000000000 --- a/tests/fixture_base_path_config/routes/_app.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { PageProps } from "$fresh/server.ts"; - -export default function App({ Component }: PageProps) { - return ( - - - - - My Fresh app - - - - - - - ); -} diff --git a/tests/fixture_base_path_config/routes/index.tsx b/tests/fixture_base_path_config/routes/index.tsx deleted file mode 100644 index d69dd510528..00000000000 --- a/tests/fixture_base_path_config/routes/index.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { Foo } from "../foo/Foo.tsx"; - -export default function Page() { - return ; -} diff --git a/tests/fixture_base_path_config/static/styles.css b/tests/fixture_base_path_config/static/styles.css deleted file mode 100644 index b5c61c95671..00000000000 --- a/tests/fixture_base_path_config/static/styles.css +++ /dev/null @@ -1,3 +0,0 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; diff --git a/tests/fixture_base_path_config/tailwind.config.ts b/tests/fixture_base_path_config/tailwind.config.ts deleted file mode 100644 index eaef5a3a7c8..00000000000 --- a/tests/fixture_base_path_config/tailwind.config.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** @type {import('tailwindcss').Config} */ -export default { - content: ["./foo/**/*.{ts,tsx}"], - theme: { - extend: { - colors: { - pp: "peachpuff", - }, - }, - }, -}; diff --git a/tests/fixture_build/deno.json b/tests/fixture_build/deno.json deleted file mode 100644 index 388be4e441e..00000000000 --- a/tests/fixture_build/deno.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "lock": false, - "imports": { - "$fresh/": "../../", - "preact": "https://esm.sh/preact@10.22.0", - "preact/": "https://esm.sh/preact@10.22.0/", - "@preact/signals": "https://esm.sh/*@preact/signals@1.1.3", - "@preact/signals-core": "https://esm.sh/@preact/signals-core@1.2.3" - }, - "compilerOptions": { - "jsx": "react-jsx", - "jsxImportSource": "preact" - } -} diff --git a/tests/fixture_build/dev.ts b/tests/fixture_build/dev.ts deleted file mode 100755 index 2d85d6c183c..00000000000 --- a/tests/fixture_build/dev.ts +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env -S deno run -A --watch=static/,routes/ - -import dev from "$fresh/dev.ts"; - -await dev(import.meta.url, "./main.ts"); diff --git a/tests/fixture_build/fresh.gen.ts b/tests/fixture_build/fresh.gen.ts deleted file mode 100644 index c25b85d9f22..00000000000 --- a/tests/fixture_build/fresh.gen.ts +++ /dev/null @@ -1,19 +0,0 @@ -// DO NOT EDIT. This file is generated by Fresh. -// This file SHOULD be checked into source version control. -// This file is automatically updated during development when running `dev.ts`. - -import * as $index from "./routes/index.tsx"; -import * as $Counter from "./islands/Counter.tsx"; -import type { Manifest } from "$fresh/server.ts"; - -const manifest = { - routes: { - "./routes/index.tsx": $index, - }, - islands: { - "./islands/Counter.tsx": $Counter, - }, - baseUrl: import.meta.url, -} satisfies Manifest; - -export default manifest; diff --git a/tests/fixture_build/islands/Counter.tsx b/tests/fixture_build/islands/Counter.tsx deleted file mode 100644 index 5f015b1ee1f..00000000000 --- a/tests/fixture_build/islands/Counter.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import type { Signal } from "@preact/signals"; -import { IS_BROWSER } from "$fresh/runtime.ts"; - -interface CounterProps { - count: Signal; - id: string; -} - -export default function Counter(props: CounterProps) { - return ( -
-

{props.count}

- -
- ); -} diff --git a/tests/fixture_build/main.ts b/tests/fixture_build/main.ts deleted file mode 100644 index dedce9cbb04..00000000000 --- a/tests/fixture_build/main.ts +++ /dev/null @@ -1,10 +0,0 @@ -/// -/// -/// -/// -/// - -import { start } from "$fresh/server.ts"; -import manifest from "./fresh.gen.ts"; - -await start(manifest); diff --git a/tests/fixture_build/routes/index.tsx b/tests/fixture_build/routes/index.tsx deleted file mode 100644 index 7473a787edb..00000000000 --- a/tests/fixture_build/routes/index.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { useSignal } from "@preact/signals"; -import Counter from "../islands/Counter.tsx"; -import { asset, Head } from "$fresh/runtime.ts"; - -export default function Home() { - const signal = useSignal(0); - return ( -
- - - - -
- ); -} diff --git a/tests/fixture_build/static/style.css b/tests/fixture_build/static/style.css deleted file mode 100644 index 573746210c7..00000000000 --- a/tests/fixture_build/static/style.css +++ /dev/null @@ -1,3 +0,0 @@ -body { - background: peachpuff; -} diff --git a/tests/fixture_build_out_dir/deno.json b/tests/fixture_build_out_dir/deno.json deleted file mode 100644 index 17b9d1ede06..00000000000 --- a/tests/fixture_build_out_dir/deno.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "lock": false, - "imports": { - "$fresh/": "../../", - "preact": "https://esm.sh/preact@10.22.0", - "preact/": "https://esm.sh/preact@10.22.0/", - "preact-render-to-string": "https://esm.sh/*preact-render-to-string@6.2.0", - "@preact/signals": "https://esm.sh/*@preact/signals@1.1.3", - "@preact/signals-core": "https://esm.sh/@preact/signals-core@1.2.3" - }, - "compilerOptions": { - "jsx": "react-jsx", - "jsxImportSource": "preact" - } -} diff --git a/tests/fixture_build_out_dir/dev.ts b/tests/fixture_build_out_dir/dev.ts deleted file mode 100755 index f2ab753ba74..00000000000 --- a/tests/fixture_build_out_dir/dev.ts +++ /dev/null @@ -1,9 +0,0 @@ -#!/usr/bin/env -S deno run -A --watch=static/,routes/ - -import dev from "$fresh/dev.ts"; - -await dev(import.meta.url, "./main.ts", { - build: { - outDir: Deno.env.get("FRESH_TEST_OUTDIR") ?? undefined, - }, -}); diff --git a/tests/fixture_build_out_dir/fresh.gen.ts b/tests/fixture_build_out_dir/fresh.gen.ts deleted file mode 100644 index c25b85d9f22..00000000000 --- a/tests/fixture_build_out_dir/fresh.gen.ts +++ /dev/null @@ -1,19 +0,0 @@ -// DO NOT EDIT. This file is generated by Fresh. -// This file SHOULD be checked into source version control. -// This file is automatically updated during development when running `dev.ts`. - -import * as $index from "./routes/index.tsx"; -import * as $Counter from "./islands/Counter.tsx"; -import type { Manifest } from "$fresh/server.ts"; - -const manifest = { - routes: { - "./routes/index.tsx": $index, - }, - islands: { - "./islands/Counter.tsx": $Counter, - }, - baseUrl: import.meta.url, -} satisfies Manifest; - -export default manifest; diff --git a/tests/fixture_build_out_dir/islands/Counter.tsx b/tests/fixture_build_out_dir/islands/Counter.tsx deleted file mode 100644 index 5f015b1ee1f..00000000000 --- a/tests/fixture_build_out_dir/islands/Counter.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import type { Signal } from "@preact/signals"; -import { IS_BROWSER } from "$fresh/runtime.ts"; - -interface CounterProps { - count: Signal; - id: string; -} - -export default function Counter(props: CounterProps) { - return ( -
-

{props.count}

- -
- ); -} diff --git a/tests/fixture_build_out_dir/main.ts b/tests/fixture_build_out_dir/main.ts deleted file mode 100644 index dedce9cbb04..00000000000 --- a/tests/fixture_build_out_dir/main.ts +++ /dev/null @@ -1,10 +0,0 @@ -/// -/// -/// -/// -/// - -import { start } from "$fresh/server.ts"; -import manifest from "./fresh.gen.ts"; - -await start(manifest); diff --git a/tests/fixture_build_out_dir/routes/index.tsx b/tests/fixture_build_out_dir/routes/index.tsx deleted file mode 100644 index 7473a787edb..00000000000 --- a/tests/fixture_build_out_dir/routes/index.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { useSignal } from "@preact/signals"; -import Counter from "../islands/Counter.tsx"; -import { asset, Head } from "$fresh/runtime.ts"; - -export default function Home() { - const signal = useSignal(0); - return ( -
- - - - -
- ); -} diff --git a/tests/fixture_build_out_dir_sub/deno.json b/tests/fixture_build_out_dir_sub/deno.json deleted file mode 100644 index 17b9d1ede06..00000000000 --- a/tests/fixture_build_out_dir_sub/deno.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "lock": false, - "imports": { - "$fresh/": "../../", - "preact": "https://esm.sh/preact@10.22.0", - "preact/": "https://esm.sh/preact@10.22.0/", - "preact-render-to-string": "https://esm.sh/*preact-render-to-string@6.2.0", - "@preact/signals": "https://esm.sh/*@preact/signals@1.1.3", - "@preact/signals-core": "https://esm.sh/@preact/signals-core@1.2.3" - }, - "compilerOptions": { - "jsx": "react-jsx", - "jsxImportSource": "preact" - } -} diff --git a/tests/fixture_build_out_dir_sub/fresh.gen.ts b/tests/fixture_build_out_dir_sub/fresh.gen.ts deleted file mode 100644 index 45819fd0e30..00000000000 --- a/tests/fixture_build_out_dir_sub/fresh.gen.ts +++ /dev/null @@ -1,13 +0,0 @@ -// DO NOT EDIT. This file is generated by Fresh. -// This file SHOULD be checked into source version control. -// This file is automatically updated during development when running `dev.ts`. - -import { type Manifest } from "$fresh/server.ts"; - -const manifest = { - routes: {}, - islands: {}, - baseUrl: import.meta.url, -} satisfies Manifest; - -export default manifest; diff --git a/tests/fixture_build_out_dir_sub/src/dev.ts b/tests/fixture_build_out_dir_sub/src/dev.ts deleted file mode 100755 index f2ab753ba74..00000000000 --- a/tests/fixture_build_out_dir_sub/src/dev.ts +++ /dev/null @@ -1,9 +0,0 @@ -#!/usr/bin/env -S deno run -A --watch=static/,routes/ - -import dev from "$fresh/dev.ts"; - -await dev(import.meta.url, "./main.ts", { - build: { - outDir: Deno.env.get("FRESH_TEST_OUTDIR") ?? undefined, - }, -}); diff --git a/tests/fixture_build_out_dir_sub/src/fresh.gen.ts b/tests/fixture_build_out_dir_sub/src/fresh.gen.ts deleted file mode 100644 index c25b85d9f22..00000000000 --- a/tests/fixture_build_out_dir_sub/src/fresh.gen.ts +++ /dev/null @@ -1,19 +0,0 @@ -// DO NOT EDIT. This file is generated by Fresh. -// This file SHOULD be checked into source version control. -// This file is automatically updated during development when running `dev.ts`. - -import * as $index from "./routes/index.tsx"; -import * as $Counter from "./islands/Counter.tsx"; -import type { Manifest } from "$fresh/server.ts"; - -const manifest = { - routes: { - "./routes/index.tsx": $index, - }, - islands: { - "./islands/Counter.tsx": $Counter, - }, - baseUrl: import.meta.url, -} satisfies Manifest; - -export default manifest; diff --git a/tests/fixture_build_out_dir_sub/src/islands/Counter.tsx b/tests/fixture_build_out_dir_sub/src/islands/Counter.tsx deleted file mode 100644 index 5f015b1ee1f..00000000000 --- a/tests/fixture_build_out_dir_sub/src/islands/Counter.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import type { Signal } from "@preact/signals"; -import { IS_BROWSER } from "$fresh/runtime.ts"; - -interface CounterProps { - count: Signal; - id: string; -} - -export default function Counter(props: CounterProps) { - return ( -
-

{props.count}

- -
- ); -} diff --git a/tests/fixture_build_out_dir_sub/src/main.ts b/tests/fixture_build_out_dir_sub/src/main.ts deleted file mode 100644 index dedce9cbb04..00000000000 --- a/tests/fixture_build_out_dir_sub/src/main.ts +++ /dev/null @@ -1,10 +0,0 @@ -/// -/// -/// -/// -/// - -import { start } from "$fresh/server.ts"; -import manifest from "./fresh.gen.ts"; - -await start(manifest); diff --git a/tests/fixture_build_out_dir_sub/src/routes/index.tsx b/tests/fixture_build_out_dir_sub/src/routes/index.tsx deleted file mode 100644 index 7473a787edb..00000000000 --- a/tests/fixture_build_out_dir_sub/src/routes/index.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { useSignal } from "@preact/signals"; -import Counter from "../islands/Counter.tsx"; -import { asset, Head } from "$fresh/runtime.ts"; - -export default function Home() { - const signal = useSignal(0); - return ( -
- - - - -
- ); -} diff --git a/tests/fixture_build_out_dir_sub2/deno.json b/tests/fixture_build_out_dir_sub2/deno.json deleted file mode 100644 index 17b9d1ede06..00000000000 --- a/tests/fixture_build_out_dir_sub2/deno.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "lock": false, - "imports": { - "$fresh/": "../../", - "preact": "https://esm.sh/preact@10.22.0", - "preact/": "https://esm.sh/preact@10.22.0/", - "preact-render-to-string": "https://esm.sh/*preact-render-to-string@6.2.0", - "@preact/signals": "https://esm.sh/*@preact/signals@1.1.3", - "@preact/signals-core": "https://esm.sh/@preact/signals-core@1.2.3" - }, - "compilerOptions": { - "jsx": "react-jsx", - "jsxImportSource": "preact" - } -} diff --git a/tests/fixture_build_out_dir_sub2/fresh.gen.ts b/tests/fixture_build_out_dir_sub2/fresh.gen.ts deleted file mode 100644 index 45819fd0e30..00000000000 --- a/tests/fixture_build_out_dir_sub2/fresh.gen.ts +++ /dev/null @@ -1,13 +0,0 @@ -// DO NOT EDIT. This file is generated by Fresh. -// This file SHOULD be checked into source version control. -// This file is automatically updated during development when running `dev.ts`. - -import { type Manifest } from "$fresh/server.ts"; - -const manifest = { - routes: {}, - islands: {}, - baseUrl: import.meta.url, -} satisfies Manifest; - -export default manifest; diff --git a/tests/fixture_build_out_dir_sub2/src/dev.ts b/tests/fixture_build_out_dir_sub2/src/dev.ts deleted file mode 100755 index f2ab753ba74..00000000000 --- a/tests/fixture_build_out_dir_sub2/src/dev.ts +++ /dev/null @@ -1,9 +0,0 @@ -#!/usr/bin/env -S deno run -A --watch=static/,routes/ - -import dev from "$fresh/dev.ts"; - -await dev(import.meta.url, "./main.ts", { - build: { - outDir: Deno.env.get("FRESH_TEST_OUTDIR") ?? undefined, - }, -}); diff --git a/tests/fixture_build_out_dir_sub2/src/fresh.gen.ts b/tests/fixture_build_out_dir_sub2/src/fresh.gen.ts deleted file mode 100644 index c25b85d9f22..00000000000 --- a/tests/fixture_build_out_dir_sub2/src/fresh.gen.ts +++ /dev/null @@ -1,19 +0,0 @@ -// DO NOT EDIT. This file is generated by Fresh. -// This file SHOULD be checked into source version control. -// This file is automatically updated during development when running `dev.ts`. - -import * as $index from "./routes/index.tsx"; -import * as $Counter from "./islands/Counter.tsx"; -import type { Manifest } from "$fresh/server.ts"; - -const manifest = { - routes: { - "./routes/index.tsx": $index, - }, - islands: { - "./islands/Counter.tsx": $Counter, - }, - baseUrl: import.meta.url, -} satisfies Manifest; - -export default manifest; diff --git a/tests/fixture_build_out_dir_sub2/src/islands/Counter.tsx b/tests/fixture_build_out_dir_sub2/src/islands/Counter.tsx deleted file mode 100644 index 5f015b1ee1f..00000000000 --- a/tests/fixture_build_out_dir_sub2/src/islands/Counter.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import type { Signal } from "@preact/signals"; -import { IS_BROWSER } from "$fresh/runtime.ts"; - -interface CounterProps { - count: Signal; - id: string; -} - -export default function Counter(props: CounterProps) { - return ( -
-

{props.count}

- -
- ); -} diff --git a/tests/fixture_build_out_dir_sub2/src/main.ts b/tests/fixture_build_out_dir_sub2/src/main.ts deleted file mode 100644 index dedce9cbb04..00000000000 --- a/tests/fixture_build_out_dir_sub2/src/main.ts +++ /dev/null @@ -1,10 +0,0 @@ -/// -/// -/// -/// -/// - -import { start } from "$fresh/server.ts"; -import manifest from "./fresh.gen.ts"; - -await start(manifest); diff --git a/tests/fixture_build_out_dir_sub2/src/routes/index.tsx b/tests/fixture_build_out_dir_sub2/src/routes/index.tsx deleted file mode 100644 index 7473a787edb..00000000000 --- a/tests/fixture_build_out_dir_sub2/src/routes/index.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { useSignal } from "@preact/signals"; -import Counter from "../islands/Counter.tsx"; -import { asset, Head } from "$fresh/runtime.ts"; - -export default function Home() { - const signal = useSignal(0); - return ( -
- - - - -
- ); -} diff --git a/tests/fixture_build_static/deno.json b/tests/fixture_build_static/deno.json deleted file mode 100644 index 388be4e441e..00000000000 --- a/tests/fixture_build_static/deno.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "lock": false, - "imports": { - "$fresh/": "../../", - "preact": "https://esm.sh/preact@10.22.0", - "preact/": "https://esm.sh/preact@10.22.0/", - "@preact/signals": "https://esm.sh/*@preact/signals@1.1.3", - "@preact/signals-core": "https://esm.sh/@preact/signals-core@1.2.3" - }, - "compilerOptions": { - "jsx": "react-jsx", - "jsxImportSource": "preact" - } -} diff --git a/tests/fixture_build_static/dev.ts b/tests/fixture_build_static/dev.ts deleted file mode 100755 index 2d85d6c183c..00000000000 --- a/tests/fixture_build_static/dev.ts +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env -S deno run -A --watch=static/,routes/ - -import dev from "$fresh/dev.ts"; - -await dev(import.meta.url, "./main.ts"); diff --git a/tests/fixture_build_static/fresh.gen.ts b/tests/fixture_build_static/fresh.gen.ts deleted file mode 100644 index b5bfec72c19..00000000000 --- a/tests/fixture_build_static/fresh.gen.ts +++ /dev/null @@ -1,17 +0,0 @@ -// DO NOT EDIT. This file is generated by Fresh. -// This file SHOULD be checked into source version control. -// This file is automatically updated during development when running `dev.ts`. - -import * as $index from "./routes/index.tsx"; - -import { type Manifest } from "$fresh/server.ts"; - -const manifest = { - routes: { - "./routes/index.tsx": $index, - }, - islands: {}, - baseUrl: import.meta.url, -} satisfies Manifest; - -export default manifest; diff --git a/tests/fixture_build_static/main.ts b/tests/fixture_build_static/main.ts deleted file mode 100644 index dedce9cbb04..00000000000 --- a/tests/fixture_build_static/main.ts +++ /dev/null @@ -1,10 +0,0 @@ -/// -/// -/// -/// -/// - -import { start } from "$fresh/server.ts"; -import manifest from "./fresh.gen.ts"; - -await start(manifest); diff --git a/tests/fixture_build_static/static/duplicate.txt b/tests/fixture_build_static/static/duplicate.txt deleted file mode 100644 index 5496f6fb0df..00000000000 --- a/tests/fixture_build_static/static/duplicate.txt +++ /dev/null @@ -1 +0,0 @@ -it doesn't work \ No newline at end of file diff --git a/tests/fixture_build_sub_dir/deno.json b/tests/fixture_build_sub_dir/deno.json deleted file mode 100644 index 17b9d1ede06..00000000000 --- a/tests/fixture_build_sub_dir/deno.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "lock": false, - "imports": { - "$fresh/": "../../", - "preact": "https://esm.sh/preact@10.22.0", - "preact/": "https://esm.sh/preact@10.22.0/", - "preact-render-to-string": "https://esm.sh/*preact-render-to-string@6.2.0", - "@preact/signals": "https://esm.sh/*@preact/signals@1.1.3", - "@preact/signals-core": "https://esm.sh/@preact/signals-core@1.2.3" - }, - "compilerOptions": { - "jsx": "react-jsx", - "jsxImportSource": "preact" - } -} diff --git a/tests/fixture_build_sub_dir/fresh.gen.ts b/tests/fixture_build_sub_dir/fresh.gen.ts deleted file mode 100644 index 45819fd0e30..00000000000 --- a/tests/fixture_build_sub_dir/fresh.gen.ts +++ /dev/null @@ -1,13 +0,0 @@ -// DO NOT EDIT. This file is generated by Fresh. -// This file SHOULD be checked into source version control. -// This file is automatically updated during development when running `dev.ts`. - -import { type Manifest } from "$fresh/server.ts"; - -const manifest = { - routes: {}, - islands: {}, - baseUrl: import.meta.url, -} satisfies Manifest; - -export default manifest; diff --git a/tests/fixture_build_sub_dir/src/dev.ts b/tests/fixture_build_sub_dir/src/dev.ts deleted file mode 100755 index 2d85d6c183c..00000000000 --- a/tests/fixture_build_sub_dir/src/dev.ts +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env -S deno run -A --watch=static/,routes/ - -import dev from "$fresh/dev.ts"; - -await dev(import.meta.url, "./main.ts"); diff --git a/tests/fixture_build_sub_dir/src/fresh.gen.ts b/tests/fixture_build_sub_dir/src/fresh.gen.ts deleted file mode 100644 index c25b85d9f22..00000000000 --- a/tests/fixture_build_sub_dir/src/fresh.gen.ts +++ /dev/null @@ -1,19 +0,0 @@ -// DO NOT EDIT. This file is generated by Fresh. -// This file SHOULD be checked into source version control. -// This file is automatically updated during development when running `dev.ts`. - -import * as $index from "./routes/index.tsx"; -import * as $Counter from "./islands/Counter.tsx"; -import type { Manifest } from "$fresh/server.ts"; - -const manifest = { - routes: { - "./routes/index.tsx": $index, - }, - islands: { - "./islands/Counter.tsx": $Counter, - }, - baseUrl: import.meta.url, -} satisfies Manifest; - -export default manifest; diff --git a/tests/fixture_build_sub_dir/src/islands/Counter.tsx b/tests/fixture_build_sub_dir/src/islands/Counter.tsx deleted file mode 100644 index 5f015b1ee1f..00000000000 --- a/tests/fixture_build_sub_dir/src/islands/Counter.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import type { Signal } from "@preact/signals"; -import { IS_BROWSER } from "$fresh/runtime.ts"; - -interface CounterProps { - count: Signal; - id: string; -} - -export default function Counter(props: CounterProps) { - return ( -
-

{props.count}

- -
- ); -} diff --git a/tests/fixture_build_sub_dir/src/main.ts b/tests/fixture_build_sub_dir/src/main.ts deleted file mode 100644 index dedce9cbb04..00000000000 --- a/tests/fixture_build_sub_dir/src/main.ts +++ /dev/null @@ -1,10 +0,0 @@ -/// -/// -/// -/// -/// - -import { start } from "$fresh/server.ts"; -import manifest from "./fresh.gen.ts"; - -await start(manifest); diff --git a/tests/fixture_build_sub_dir/src/routes/index.tsx b/tests/fixture_build_sub_dir/src/routes/index.tsx deleted file mode 100644 index 7473a787edb..00000000000 --- a/tests/fixture_build_sub_dir/src/routes/index.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { useSignal } from "@preact/signals"; -import Counter from "../islands/Counter.tsx"; -import { asset, Head } from "$fresh/runtime.ts"; - -export default function Home() { - const signal = useSignal(0); - return ( -
- - - - -
- ); -} diff --git a/tests/fixture_build_sub_dir/src/static/style.css b/tests/fixture_build_sub_dir/src/static/style.css deleted file mode 100644 index 573746210c7..00000000000 --- a/tests/fixture_build_sub_dir/src/static/style.css +++ /dev/null @@ -1,3 +0,0 @@ -body { - background: peachpuff; -} diff --git a/tests/fixture_build_target/deno.json b/tests/fixture_build_target/deno.json deleted file mode 100644 index 17b9d1ede06..00000000000 --- a/tests/fixture_build_target/deno.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "lock": false, - "imports": { - "$fresh/": "../../", - "preact": "https://esm.sh/preact@10.22.0", - "preact/": "https://esm.sh/preact@10.22.0/", - "preact-render-to-string": "https://esm.sh/*preact-render-to-string@6.2.0", - "@preact/signals": "https://esm.sh/*@preact/signals@1.1.3", - "@preact/signals-core": "https://esm.sh/@preact/signals-core@1.2.3" - }, - "compilerOptions": { - "jsx": "react-jsx", - "jsxImportSource": "preact" - } -} diff --git a/tests/fixture_build_target/dev.ts b/tests/fixture_build_target/dev.ts deleted file mode 100755 index 6fc1d8ebf20..00000000000 --- a/tests/fixture_build_target/dev.ts +++ /dev/null @@ -1,9 +0,0 @@ -#!/usr/bin/env -S deno run -A --watch=static/,routes/ - -import dev from "$fresh/dev.ts"; - -await dev(import.meta.url, "./main.ts", { - build: { - target: Deno.env.get("FRESH_TEST_TARGET"), - }, -}); diff --git a/tests/fixture_build_target/fresh.gen.ts b/tests/fixture_build_target/fresh.gen.ts deleted file mode 100644 index c25b85d9f22..00000000000 --- a/tests/fixture_build_target/fresh.gen.ts +++ /dev/null @@ -1,19 +0,0 @@ -// DO NOT EDIT. This file is generated by Fresh. -// This file SHOULD be checked into source version control. -// This file is automatically updated during development when running `dev.ts`. - -import * as $index from "./routes/index.tsx"; -import * as $Counter from "./islands/Counter.tsx"; -import type { Manifest } from "$fresh/server.ts"; - -const manifest = { - routes: { - "./routes/index.tsx": $index, - }, - islands: { - "./islands/Counter.tsx": $Counter, - }, - baseUrl: import.meta.url, -} satisfies Manifest; - -export default manifest; diff --git a/tests/fixture_build_target/islands/Counter.tsx b/tests/fixture_build_target/islands/Counter.tsx deleted file mode 100644 index a59caf91896..00000000000 --- a/tests/fixture_build_target/islands/Counter.tsx +++ /dev/null @@ -1,4 +0,0 @@ -export default function Counter(props: { text?: string }) { - const text = props.text ?? "check output"; - return

{text}

; -} diff --git a/tests/fixture_build_target/main.ts b/tests/fixture_build_target/main.ts deleted file mode 100644 index dedce9cbb04..00000000000 --- a/tests/fixture_build_target/main.ts +++ /dev/null @@ -1,10 +0,0 @@ -/// -/// -/// -/// -/// - -import { start } from "$fresh/server.ts"; -import manifest from "./fresh.gen.ts"; - -await start(manifest); diff --git a/tests/fixture_build_target/routes/index.tsx b/tests/fixture_build_target/routes/index.tsx deleted file mode 100644 index efaa376121a..00000000000 --- a/tests/fixture_build_target/routes/index.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import Counter from "../islands/Counter.tsx"; - -export default function Home() { - return ; -} diff --git a/tests/fixture_config/deno.json b/tests/fixture_config/deno.json deleted file mode 100644 index 17b9d1ede06..00000000000 --- a/tests/fixture_config/deno.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "lock": false, - "imports": { - "$fresh/": "../../", - "preact": "https://esm.sh/preact@10.22.0", - "preact/": "https://esm.sh/preact@10.22.0/", - "preact-render-to-string": "https://esm.sh/*preact-render-to-string@6.2.0", - "@preact/signals": "https://esm.sh/*@preact/signals@1.1.3", - "@preact/signals-core": "https://esm.sh/@preact/signals-core@1.2.3" - }, - "compilerOptions": { - "jsx": "react-jsx", - "jsxImportSource": "preact" - } -} diff --git a/tests/fixture_config/dev.ts b/tests/fixture_config/dev.ts deleted file mode 100755 index 59dfdfdea1a..00000000000 --- a/tests/fixture_config/dev.ts +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env -S deno run -A --watch=static/,routes/ - -import dev from "$fresh/dev.ts"; - -const TEST_CONFIG_SERVER = Deno.env.get("TEST_CONFIG_SERVER") === "true"; -const onListen = (params: { hostname: string; port: number }) => { - console.log("it works"); - console.log(`http://localhost:${params.port}`); -}; -const onListen2 = (params: { hostname: string; port: number }) => { - console.log("it works #2"); - console.log(`http://localhost:${params.port}`); -}; - -await dev(import.meta.url, "./main.ts", { - server: { - onListen: TEST_CONFIG_SERVER ? onListen2 : undefined, - }, - onListen: TEST_CONFIG_SERVER ? undefined : onListen, -}); diff --git a/tests/fixture_config/fresh.gen.ts b/tests/fixture_config/fresh.gen.ts deleted file mode 100644 index 5f36a456139..00000000000 --- a/tests/fixture_config/fresh.gen.ts +++ /dev/null @@ -1,17 +0,0 @@ -// DO NOT EDIT. This file is generated by Fresh. -// This file SHOULD be checked into source version control. -// This file is automatically updated during development when running `dev.ts`. - -import * as $index from "./routes/index.tsx"; - -import type { Manifest } from "$fresh/server.ts"; - -const manifest = { - routes: { - "./routes/index.tsx": $index, - }, - islands: {}, - baseUrl: import.meta.url, -} satisfies Manifest; - -export default manifest; diff --git a/tests/fixture_config/main.ts b/tests/fixture_config/main.ts deleted file mode 100644 index edbe2b0a921..00000000000 --- a/tests/fixture_config/main.ts +++ /dev/null @@ -1,25 +0,0 @@ -/// -/// -/// -/// -/// - -import { start } from "$fresh/server.ts"; -import manifest from "./fresh.gen.ts"; - -const TEST_CONFIG_SERVER = Deno.env.get("TEST_CONFIG_SERVER") === "true"; -const onListen = (params: { hostname: string; port: number }) => { - console.log("it works"); - console.log(`http://localhost:${params.port}`); -}; -const onListen2 = (params: { hostname: string; port: number }) => { - console.log("it works #2"); - console.log(`http://localhost:${params.port}`); -}; - -await start(manifest, { - server: { - onListen: TEST_CONFIG_SERVER ? onListen2 : undefined, - }, - onListen: TEST_CONFIG_SERVER ? undefined : onListen, -}); diff --git a/tests/fixture_config/routes/index.tsx b/tests/fixture_config/routes/index.tsx deleted file mode 100644 index 9025646dc1f..00000000000 --- a/tests/fixture_config/routes/index.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export default function Home() { - return

hello

; -} diff --git a/tests/fixture_custom_500/deno.json b/tests/fixture_custom_500/deno.json deleted file mode 100644 index b211f1f4233..00000000000 --- a/tests/fixture_custom_500/deno.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "lock": false, - "imports": { - "$fresh/": "../../", - "preact": "https://esm.sh/preact@10.22.0", - "preact/": "https://esm.sh/preact@10.22.0/" - }, - "compilerOptions": { - "jsx": "react-jsx", - "jsxImportSource": "preact" - } -} diff --git a/tests/fixture_custom_500/dev.ts b/tests/fixture_custom_500/dev.ts deleted file mode 100755 index 2d85d6c183c..00000000000 --- a/tests/fixture_custom_500/dev.ts +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env -S deno run -A --watch=static/,routes/ - -import dev from "$fresh/dev.ts"; - -await dev(import.meta.url, "./main.ts"); diff --git a/tests/fixture_custom_500/fresh.gen.ts b/tests/fixture_custom_500/fresh.gen.ts deleted file mode 100644 index 666a0cdb4be..00000000000 --- a/tests/fixture_custom_500/fresh.gen.ts +++ /dev/null @@ -1,19 +0,0 @@ -// DO NOT EDIT. This file is generated by Fresh. -// This file SHOULD be checked into source version control. -// This file is automatically updated during development when running `dev.ts`. - -import * as $_500 from "./routes/_500.tsx"; -import * as $index from "./routes/index.tsx"; - -import { type Manifest } from "$fresh/server.ts"; - -const manifest = { - routes: { - "./routes/_500.tsx": $_500, - "./routes/index.tsx": $index, - }, - islands: {}, - baseUrl: import.meta.url, -} satisfies Manifest; - -export default manifest; diff --git a/tests/fixture_custom_500/main.ts b/tests/fixture_custom_500/main.ts deleted file mode 100644 index dedce9cbb04..00000000000 --- a/tests/fixture_custom_500/main.ts +++ /dev/null @@ -1,10 +0,0 @@ -/// -/// -/// -/// -/// - -import { start } from "$fresh/server.ts"; -import manifest from "./fresh.gen.ts"; - -await start(manifest); diff --git a/tests/fixture_custom_500/routes/_500.tsx b/tests/fixture_custom_500/routes/_500.tsx deleted file mode 100644 index 164e8299a30..00000000000 --- a/tests/fixture_custom_500/routes/_500.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { ErrorHandler, PageProps } from "../../../server.ts"; - -export const handler: ErrorHandler = (_req, ctx) => { - return ctx.render(); -}; - -export default function Error500Page({ error }: PageProps) { - return

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

; -} diff --git a/tests/fixture_custom_500/routes/index.tsx b/tests/fixture_custom_500/routes/index.tsx deleted file mode 100644 index 24b9a6c26e5..00000000000 --- a/tests/fixture_custom_500/routes/index.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import { Handlers } from "../../../server.ts"; - -export const handler: Handlers = { - GET(_req, ctx) { - throw new Error("Pickle Rick!"); - }, -}; diff --git a/tests/fixture_define_helpers/dev.ts b/tests/fixture_define_helpers/dev.ts deleted file mode 100755 index 2d85d6c183c..00000000000 --- a/tests/fixture_define_helpers/dev.ts +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env -S deno run -A --watch=static/,routes/ - -import dev from "$fresh/dev.ts"; - -await dev(import.meta.url, "./main.ts"); diff --git a/tests/fixture_define_helpers/fresh.gen.ts b/tests/fixture_define_helpers/fresh.gen.ts deleted file mode 100644 index e902f3b07eb..00000000000 --- a/tests/fixture_define_helpers/fresh.gen.ts +++ /dev/null @@ -1,21 +0,0 @@ -// DO NOT EDIT. This file is generated by Fresh. -// This file SHOULD be checked into source version control. -// This file is automatically updated during development when running `dev.ts`. - -import * as $_app from "./routes/_app.tsx"; -import * as $_layout from "./routes/_layout.tsx"; -import * as $index from "./routes/index.tsx"; - -import { type Manifest } from "$fresh/server.ts"; - -const manifest = { - routes: { - "./routes/_app.tsx": $_app, - "./routes/_layout.tsx": $_layout, - "./routes/index.tsx": $index, - }, - islands: {}, - baseUrl: import.meta.url, -} satisfies Manifest; - -export default manifest; diff --git a/tests/fixture_define_helpers/main.ts b/tests/fixture_define_helpers/main.ts deleted file mode 100644 index dedce9cbb04..00000000000 --- a/tests/fixture_define_helpers/main.ts +++ /dev/null @@ -1,10 +0,0 @@ -/// -/// -/// -/// -/// - -import { start } from "$fresh/server.ts"; -import manifest from "./fresh.gen.ts"; - -await start(manifest); diff --git a/tests/fixture_define_helpers/other/state.ts b/tests/fixture_define_helpers/other/state.ts deleted file mode 100644 index 3bd5a0d3a0f..00000000000 --- a/tests/fixture_define_helpers/other/state.ts +++ /dev/null @@ -1 +0,0 @@ -export type State = { something: string }; diff --git a/tests/fixture_define_helpers/routes/_app.tsx b/tests/fixture_define_helpers/routes/_app.tsx deleted file mode 100644 index fbf1a219a19..00000000000 --- a/tests/fixture_define_helpers/routes/_app.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { defineApp } from "$fresh/server.ts"; -import { State } from "../other/state.ts"; - -export default defineApp((req, ctx) => { - ctx.state.something = "foo"; - return ( -
- -
- ); -}); diff --git a/tests/fixture_define_helpers/routes/_layout.tsx b/tests/fixture_define_helpers/routes/_layout.tsx deleted file mode 100644 index 3d4e5a0d41c..00000000000 --- a/tests/fixture_define_helpers/routes/_layout.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { defineLayout } from "$fresh/server.ts"; -import { State } from "../other/state.ts"; - -export default defineLayout((req, ctx) => { - return ( -
-

- Layout: {ctx.state.something === "foo" ? "it works" : "it doesn't work"} -

- -
- ); -}); diff --git a/tests/fixture_define_helpers/routes/index.tsx b/tests/fixture_define_helpers/routes/index.tsx deleted file mode 100644 index bde2b5da4ff..00000000000 --- a/tests/fixture_define_helpers/routes/index.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { defineRoute } from "$fresh/server.ts"; -import { delay } from "$fresh/tests/deps.ts"; -import { State } from "../other/state.ts"; - -export default defineRoute(async (req, ctx) => { - await delay(10); - return ( -
-

- Page: {ctx.state.something === "foo" ? "it works" : "it doesn't work"} -

-
- ); -}); diff --git a/tests/fixture_dev_codeframe/dev.ts b/tests/fixture_dev_codeframe/dev.ts deleted file mode 100644 index 1b53e2e77cf..00000000000 --- a/tests/fixture_dev_codeframe/dev.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { dev } from "$fresh/src/dev/dev_command.ts"; -import config from "./fresh.config.ts"; - -await dev(import.meta.url, "./main.ts", config); diff --git a/tests/fixture_dev_codeframe/fresh.config.ts b/tests/fixture_dev_codeframe/fresh.config.ts deleted file mode 100644 index 889a6862f1f..00000000000 --- a/tests/fixture_dev_codeframe/fresh.config.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { defineConfig } from "$fresh/server.ts"; -import twind from "$fresh/plugins/twind.ts"; - -export default defineConfig({ - plugins: [twind({ - selfURL: import.meta.url, - })], -}); diff --git a/tests/fixture_dev_codeframe/fresh.gen.ts b/tests/fixture_dev_codeframe/fresh.gen.ts deleted file mode 100644 index 666a0cdb4be..00000000000 --- a/tests/fixture_dev_codeframe/fresh.gen.ts +++ /dev/null @@ -1,19 +0,0 @@ -// DO NOT EDIT. This file is generated by Fresh. -// This file SHOULD be checked into source version control. -// This file is automatically updated during development when running `dev.ts`. - -import * as $_500 from "./routes/_500.tsx"; -import * as $index from "./routes/index.tsx"; - -import { type Manifest } from "$fresh/server.ts"; - -const manifest = { - routes: { - "./routes/_500.tsx": $_500, - "./routes/index.tsx": $index, - }, - islands: {}, - baseUrl: import.meta.url, -} satisfies Manifest; - -export default manifest; diff --git a/tests/fixture_dev_codeframe/main.ts b/tests/fixture_dev_codeframe/main.ts deleted file mode 100644 index fc9359215e3..00000000000 --- a/tests/fixture_dev_codeframe/main.ts +++ /dev/null @@ -1,11 +0,0 @@ -/// -/// -/// -/// -/// - -import { start } from "$fresh/server.ts"; -import manifest from "./fresh.gen.ts"; -import config from "./fresh.config.ts"; - -await start(manifest, config); diff --git a/tests/fixture_dev_codeframe/routes/_500.tsx b/tests/fixture_dev_codeframe/routes/_500.tsx deleted file mode 100644 index 97672e97c4f..00000000000 --- a/tests/fixture_dev_codeframe/routes/_500.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export default function Error() { - return

500 page

; -} diff --git a/tests/fixture_dev_codeframe/routes/index.tsx b/tests/fixture_dev_codeframe/routes/index.tsx deleted file mode 100644 index d9aa8fce0d2..00000000000 --- a/tests/fixture_dev_codeframe/routes/index.tsx +++ /dev/null @@ -1,4 +0,0 @@ -// deno-lint-ignore no-explicit-any -export default function Page(): any { - throw new Error("fail"); -} diff --git a/tests/fixture_dev_config/dev.ts b/tests/fixture_dev_config/dev.ts deleted file mode 100644 index 1b53e2e77cf..00000000000 --- a/tests/fixture_dev_config/dev.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { dev } from "$fresh/src/dev/dev_command.ts"; -import config from "./fresh.config.ts"; - -await dev(import.meta.url, "./main.ts", config); diff --git a/tests/fixture_dev_config/fresh.config.ts b/tests/fixture_dev_config/fresh.config.ts deleted file mode 100644 index 889a6862f1f..00000000000 --- a/tests/fixture_dev_config/fresh.config.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { defineConfig } from "$fresh/server.ts"; -import twind from "$fresh/plugins/twind.ts"; - -export default defineConfig({ - plugins: [twind({ - selfURL: import.meta.url, - })], -}); diff --git a/tests/fixture_dev_config/fresh.gen.ts b/tests/fixture_dev_config/fresh.gen.ts deleted file mode 100644 index a2421385a6c..00000000000 --- a/tests/fixture_dev_config/fresh.gen.ts +++ /dev/null @@ -1,19 +0,0 @@ -// DO NOT EDIT. This file is generated by Fresh. -// This file SHOULD be checked into source version control. -// This file is automatically updated during development when running `dev.ts`. - -import * as $codeframe from "./routes/codeframe.tsx"; -import * as $index from "./routes/index.tsx"; - -import { type Manifest } from "$fresh/server.ts"; - -const manifest = { - routes: { - "./routes/codeframe.tsx": $codeframe, - "./routes/index.tsx": $index, - }, - islands: {}, - baseUrl: import.meta.url, -} satisfies Manifest; - -export default manifest; diff --git a/tests/fixture_dev_config/main.ts b/tests/fixture_dev_config/main.ts deleted file mode 100644 index fc9359215e3..00000000000 --- a/tests/fixture_dev_config/main.ts +++ /dev/null @@ -1,11 +0,0 @@ -/// -/// -/// -/// -/// - -import { start } from "$fresh/server.ts"; -import manifest from "./fresh.gen.ts"; -import config from "./fresh.config.ts"; - -await start(manifest, config); diff --git a/tests/fixture_dev_config/routes/codeframe.tsx b/tests/fixture_dev_config/routes/codeframe.tsx deleted file mode 100644 index d9aa8fce0d2..00000000000 --- a/tests/fixture_dev_config/routes/codeframe.tsx +++ /dev/null @@ -1,4 +0,0 @@ -// deno-lint-ignore no-explicit-any -export default function Page(): any { - throw new Error("fail"); -} diff --git a/tests/fixture_dev_config/routes/index.tsx b/tests/fixture_dev_config/routes/index.tsx deleted file mode 100644 index d549f8ae630..00000000000 --- a/tests/fixture_dev_config/routes/index.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export default function Page() { - return

check color

; -} diff --git a/tests/fixture_dev_legacy/dev.ts b/tests/fixture_dev_legacy/dev.ts deleted file mode 100644 index dfd26db6721..00000000000 --- a/tests/fixture_dev_legacy/dev.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { dev } from "$fresh/src/dev/dev_command.ts"; - -await dev(import.meta.url, "./main.ts"); diff --git a/tests/fixture_dev_legacy/fresh.gen.ts b/tests/fixture_dev_legacy/fresh.gen.ts deleted file mode 100644 index a2421385a6c..00000000000 --- a/tests/fixture_dev_legacy/fresh.gen.ts +++ /dev/null @@ -1,19 +0,0 @@ -// DO NOT EDIT. This file is generated by Fresh. -// This file SHOULD be checked into source version control. -// This file is automatically updated during development when running `dev.ts`. - -import * as $codeframe from "./routes/codeframe.tsx"; -import * as $index from "./routes/index.tsx"; - -import { type Manifest } from "$fresh/server.ts"; - -const manifest = { - routes: { - "./routes/codeframe.tsx": $codeframe, - "./routes/index.tsx": $index, - }, - islands: {}, - baseUrl: import.meta.url, -} satisfies Manifest; - -export default manifest; diff --git a/tests/fixture_dev_legacy/main.ts b/tests/fixture_dev_legacy/main.ts deleted file mode 100644 index 0cb2376a08f..00000000000 --- a/tests/fixture_dev_legacy/main.ts +++ /dev/null @@ -1,14 +0,0 @@ -/// -/// -/// -/// -/// - -import { start } from "$fresh/server.ts"; -import manifest from "./fresh.gen.ts"; -import twind from "$fresh/plugins/twind.ts"; -import twindConfig from "./twind.config.ts"; - -await start(manifest, { - plugins: [twind(twindConfig)], -}); diff --git a/tests/fixture_dev_legacy/routes/codeframe.tsx b/tests/fixture_dev_legacy/routes/codeframe.tsx deleted file mode 100644 index d9aa8fce0d2..00000000000 --- a/tests/fixture_dev_legacy/routes/codeframe.tsx +++ /dev/null @@ -1,4 +0,0 @@ -// deno-lint-ignore no-explicit-any -export default function Page(): any { - throw new Error("fail"); -} diff --git a/tests/fixture_dev_legacy/routes/index.tsx b/tests/fixture_dev_legacy/routes/index.tsx deleted file mode 100644 index d549f8ae630..00000000000 --- a/tests/fixture_dev_legacy/routes/index.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export default function Page() { - return

check color

; -} diff --git a/tests/fixture_dev_legacy/twind.config.ts b/tests/fixture_dev_legacy/twind.config.ts deleted file mode 100644 index e46028062d7..00000000000 --- a/tests/fixture_dev_legacy/twind.config.ts +++ /dev/null @@ -1,3 +0,0 @@ -export default { - selfURL: import.meta.url, -}; diff --git a/tests/fixture_error/deno.json b/tests/fixture_error/deno.json deleted file mode 100644 index b211f1f4233..00000000000 --- a/tests/fixture_error/deno.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "lock": false, - "imports": { - "$fresh/": "../../", - "preact": "https://esm.sh/preact@10.22.0", - "preact/": "https://esm.sh/preact@10.22.0/" - }, - "compilerOptions": { - "jsx": "react-jsx", - "jsxImportSource": "preact" - } -} diff --git a/tests/fixture_error/dev.ts b/tests/fixture_error/dev.ts deleted file mode 100755 index 2d85d6c183c..00000000000 --- a/tests/fixture_error/dev.ts +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env -S deno run -A --watch=static/,routes/ - -import dev from "$fresh/dev.ts"; - -await dev(import.meta.url, "./main.ts"); diff --git a/tests/fixture_error/fresh.gen.ts b/tests/fixture_error/fresh.gen.ts deleted file mode 100644 index 470ec1ca105..00000000000 --- a/tests/fixture_error/fresh.gen.ts +++ /dev/null @@ -1,21 +0,0 @@ -// DO NOT EDIT. This file is generated by Fresh. -// This file SHOULD be checked into source version control. -// This file is automatically updated during development when running `dev.ts`. - -import * as $_all_ from "./routes/[...all].ts"; -import * as $_500 from "./routes/_500.tsx"; -import * as $index from "./routes/index.tsx"; - -import { type Manifest } from "$fresh/server.ts"; - -const manifest = { - routes: { - "./routes/[...all].ts": $_all_, - "./routes/_500.tsx": $_500, - "./routes/index.tsx": $index, - }, - islands: {}, - baseUrl: import.meta.url, -} satisfies Manifest; - -export default manifest; diff --git a/tests/fixture_error/main.ts b/tests/fixture_error/main.ts deleted file mode 100644 index dedce9cbb04..00000000000 --- a/tests/fixture_error/main.ts +++ /dev/null @@ -1,10 +0,0 @@ -/// -/// -/// -/// -/// - -import { start } from "$fresh/server.ts"; -import manifest from "./fresh.gen.ts"; - -await start(manifest); diff --git a/tests/fixture_error/routes/[...all].ts b/tests/fixture_error/routes/[...all].ts deleted file mode 100644 index dd4355b9518..00000000000 --- a/tests/fixture_error/routes/[...all].ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Handlers } from "../../../server.ts"; - -export const handler: Handlers = { - GET(_req, ctx) { - return new Response(ctx.params.all); - }, -}; diff --git a/tests/fixture_error/routes/_500.tsx b/tests/fixture_error/routes/_500.tsx deleted file mode 100644 index 39e72fa5bcd..00000000000 --- a/tests/fixture_error/routes/_500.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export default function Error500() { - return

500 page

; -} diff --git a/tests/fixture_error/routes/index.tsx b/tests/fixture_error/routes/index.tsx deleted file mode 100644 index fcaf0efc9e7..00000000000 --- a/tests/fixture_error/routes/index.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export default function Home(): null { - throw new Error("boom!"); -} diff --git a/tests/fixture_explicit_app/dev.ts b/tests/fixture_explicit_app/dev.ts deleted file mode 100644 index 2d85d6c183c..00000000000 --- a/tests/fixture_explicit_app/dev.ts +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env -S deno run -A --watch=static/,routes/ - -import dev from "$fresh/dev.ts"; - -await dev(import.meta.url, "./main.ts"); diff --git a/tests/fixture_explicit_app/fresh.gen.ts b/tests/fixture_explicit_app/fresh.gen.ts deleted file mode 100644 index e8eb8e6cb02..00000000000 --- a/tests/fixture_explicit_app/fresh.gen.ts +++ /dev/null @@ -1,23 +0,0 @@ -// DO NOT EDIT. This file is generated by Fresh. -// This file SHOULD be checked into source version control. -// This file is automatically updated during development when running `dev.ts`. - -import * as $_app from "./routes/_app.tsx"; -import * as $head from "./routes/head.tsx"; -import * as $index from "./routes/index.tsx"; -import * as $title from "./routes/title.tsx"; - -import { type Manifest } from "$fresh/server.ts"; - -const manifest = { - routes: { - "./routes/_app.tsx": $_app, - "./routes/head.tsx": $head, - "./routes/index.tsx": $index, - "./routes/title.tsx": $title, - }, - islands: {}, - baseUrl: import.meta.url, -} satisfies Manifest; - -export default manifest; diff --git a/tests/fixture_explicit_app/main.ts b/tests/fixture_explicit_app/main.ts deleted file mode 100644 index 0f15e8defa4..00000000000 --- a/tests/fixture_explicit_app/main.ts +++ /dev/null @@ -1,10 +0,0 @@ -/// -/// -/// -/// -/// - -import { start } from "$fresh/server.ts"; -import manifest from "./fresh.gen.ts"; - -await start(manifest); diff --git a/tests/fixture_explicit_app/routes/_app.tsx b/tests/fixture_explicit_app/routes/_app.tsx deleted file mode 100644 index b366ba0199d..00000000000 --- a/tests/fixture_explicit_app/routes/_app.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { Handler, PageProps } from "$fresh/server.ts"; - -export const handler: Handler = (_req, ctx) => { - ctx.state.lang = "de"; - return ctx.render(); -}; - -export default function App( - { Component, state }: PageProps, -) { - return ( - - - - - fresh title - - -
- -
- - - ); -} diff --git a/tests/fixture_explicit_app/routes/head.tsx b/tests/fixture_explicit_app/routes/head.tsx deleted file mode 100644 index 82e2fdd193e..00000000000 --- a/tests/fixture_explicit_app/routes/head.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { Head } from "$fresh/runtime.ts"; - -export default function Home() { - return ( -
- - - - /head -
- ); -} diff --git a/tests/fixture_explicit_app/routes/index.tsx b/tests/fixture_explicit_app/routes/index.tsx deleted file mode 100644 index 7f6cc82f7da..00000000000 --- a/tests/fixture_explicit_app/routes/index.tsx +++ /dev/null @@ -1,7 +0,0 @@ -export default function Home() { - return ( -
- Home -
- ); -} diff --git a/tests/fixture_explicit_app/routes/title.tsx b/tests/fixture_explicit_app/routes/title.tsx deleted file mode 100644 index cdf4e7767c6..00000000000 --- a/tests/fixture_explicit_app/routes/title.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { Head } from "$fresh/runtime.ts"; - -export default function Home() { - return ( -
- - foo bar - - /title -
- ); -} diff --git a/tests/fixture_group_index/deno.json b/tests/fixture_group_index/deno.json deleted file mode 100644 index 9d48eadf1c3..00000000000 --- a/tests/fixture_group_index/deno.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "lock": false, - "imports": { - "$fresh/": "../../", - "preact": "https://esm.sh/preact@10.22.0", - "preact/": "https://esm.sh/preact@10.22.0/", - "preact-render-to-string": "https://esm.sh/*preact-render-to-string@6.2.1" - }, - "compilerOptions": { - "jsx": "react-jsx", - "jsxImportSource": "preact" - } -} diff --git a/tests/fixture_group_index/dev.ts b/tests/fixture_group_index/dev.ts deleted file mode 100755 index 2d85d6c183c..00000000000 --- a/tests/fixture_group_index/dev.ts +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env -S deno run -A --watch=static/,routes/ - -import dev from "$fresh/dev.ts"; - -await dev(import.meta.url, "./main.ts"); diff --git a/tests/fixture_group_index/fresh.gen.ts b/tests/fixture_group_index/fresh.gen.ts deleted file mode 100644 index d2700fdd9c4..00000000000 --- a/tests/fixture_group_index/fresh.gen.ts +++ /dev/null @@ -1,17 +0,0 @@ -// DO NOT EDIT. This file is generated by Fresh. -// This file SHOULD be checked into source version control. -// This file is automatically updated during development when running `dev.ts`. - -import * as $_group_index from "./routes/(group)/index.tsx"; - -import { type Manifest } from "$fresh/server.ts"; - -const manifest = { - routes: { - "./routes/(group)/index.tsx": $_group_index, - }, - islands: {}, - baseUrl: import.meta.url, -} satisfies Manifest; - -export default manifest; diff --git a/tests/fixture_group_index/main.ts b/tests/fixture_group_index/main.ts deleted file mode 100644 index dedce9cbb04..00000000000 --- a/tests/fixture_group_index/main.ts +++ /dev/null @@ -1,10 +0,0 @@ -/// -/// -/// -/// -/// - -import { start } from "$fresh/server.ts"; -import manifest from "./fresh.gen.ts"; - -await start(manifest); diff --git a/tests/fixture_group_index/routes/(group)/index.tsx b/tests/fixture_group_index/routes/(group)/index.tsx deleted file mode 100644 index 5048322b358..00000000000 --- a/tests/fixture_group_index/routes/(group)/index.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export default function Page() { - return

it works

; -} diff --git a/tests/fixture_hmr/deno.json b/tests/fixture_hmr/deno.json deleted file mode 100644 index ec89c30c61a..00000000000 --- a/tests/fixture_hmr/deno.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "lock": false, - "imports": { - "$fresh/": "../../", - "preact": "https://esm.sh/preact@10.22.0", - "preact/": "https://esm.sh/preact@10.22.0/", - "preact-render-to-string": "https://esm.sh/*preact-render-to-string@6.2.1", - "@preact/signals": "https://esm.sh/*@preact/signals@1.1.5", - "@preact/signals-core": "https://esm.sh/@preact/signals-core@1.3.1" - }, - "compilerOptions": { - "jsx": "react-jsx", - "jsxImportSource": "preact" - } -} diff --git a/tests/fixture_hmr/dev.ts b/tests/fixture_hmr/dev.ts deleted file mode 100755 index 2d85d6c183c..00000000000 --- a/tests/fixture_hmr/dev.ts +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env -S deno run -A --watch=static/,routes/ - -import dev from "$fresh/dev.ts"; - -await dev(import.meta.url, "./main.ts"); diff --git a/tests/fixture_hmr/fresh.gen.ts b/tests/fixture_hmr/fresh.gen.ts deleted file mode 100644 index c5634738a5f..00000000000 --- a/tests/fixture_hmr/fresh.gen.ts +++ /dev/null @@ -1,23 +0,0 @@ -// DO NOT EDIT. This file is generated by Fresh. -// This file SHOULD be checked into source version control. -// This file is automatically updated during development when running `dev.ts`. - -import * as $index from "./routes/index.tsx"; -import * as $island from "./routes/island.tsx"; -import * as $no_island from "./routes/no_island.tsx"; -import * as $Counter from "./islands/Counter.tsx"; -import { type Manifest } from "$fresh/server.ts"; - -const manifest = { - routes: { - "./routes/index.tsx": $index, - "./routes/island.tsx": $island, - "./routes/no_island.tsx": $no_island, - }, - islands: { - "./islands/Counter.tsx": $Counter, - }, - baseUrl: import.meta.url, -} satisfies Manifest; - -export default manifest; diff --git a/tests/fixture_hmr/islands/Counter.tsx b/tests/fixture_hmr/islands/Counter.tsx deleted file mode 100644 index 9e3f9244542..00000000000 --- a/tests/fixture_hmr/islands/Counter.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { useSignal } from "@preact/signals"; - -export default function Counter() { - const sig = useSignal(0); - - return ( -
-

{sig}

- -
- ); -} diff --git a/tests/fixture_hmr/main.ts b/tests/fixture_hmr/main.ts deleted file mode 100644 index dedce9cbb04..00000000000 --- a/tests/fixture_hmr/main.ts +++ /dev/null @@ -1,10 +0,0 @@ -/// -/// -/// -/// -/// - -import { start } from "$fresh/server.ts"; -import manifest from "./fresh.gen.ts"; - -await start(manifest); diff --git a/tests/fixture_hmr/routes/index.tsx b/tests/fixture_hmr/routes/index.tsx deleted file mode 100644 index d50c5fee82e..00000000000 --- a/tests/fixture_hmr/routes/index.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { basename, dirname, extname, fromFileUrl } from "../../deps.ts"; - -const __dirname = dirname(fromFileUrl(import.meta.url)); - -const links: string[] = []; -for (const file of Deno.readDirSync(__dirname)) { - if (file.name.startsWith("index")) continue; - const name = basename(file.name, extname(file.name)); - links.push(name); -} - -export default function Home() { - return ( -
-

Tests

-
    - {links.sort().map((link) => { - return ( -
  • - {link} -
  • - ); - })} -
-
- ); -} diff --git a/tests/fixture_hmr/routes/island.tsx b/tests/fixture_hmr/routes/island.tsx deleted file mode 100644 index f268243038b..00000000000 --- a/tests/fixture_hmr/routes/island.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import Counter from "../islands/Counter.tsx"; - -export default function Home() { - return ( -
-

foo

- -
- ); -} diff --git a/tests/fixture_hmr/routes/no_island.tsx b/tests/fixture_hmr/routes/no_island.tsx deleted file mode 100644 index e8027d58f30..00000000000 --- a/tests/fixture_hmr/routes/no_island.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export default function Home() { - return

foo

; -} diff --git a/tests/fixture_invalid_handlers/deno.json b/tests/fixture_invalid_handlers/deno.json deleted file mode 100644 index b211f1f4233..00000000000 --- a/tests/fixture_invalid_handlers/deno.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "lock": false, - "imports": { - "$fresh/": "../../", - "preact": "https://esm.sh/preact@10.22.0", - "preact/": "https://esm.sh/preact@10.22.0/" - }, - "compilerOptions": { - "jsx": "react-jsx", - "jsxImportSource": "preact" - } -} diff --git a/tests/fixture_invalid_handlers/dev.ts b/tests/fixture_invalid_handlers/dev.ts deleted file mode 100755 index 2d85d6c183c..00000000000 --- a/tests/fixture_invalid_handlers/dev.ts +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env -S deno run -A --watch=static/,routes/ - -import dev from "$fresh/dev.ts"; - -await dev(import.meta.url, "./main.ts"); diff --git a/tests/fixture_invalid_handlers/fresh.gen.ts b/tests/fixture_invalid_handlers/fresh.gen.ts deleted file mode 100644 index 2ff7a309804..00000000000 --- a/tests/fixture_invalid_handlers/fresh.gen.ts +++ /dev/null @@ -1,15 +0,0 @@ -// DO NOT EDIT. This file is generated by Fresh. -// This file SHOULD be checked into source version control. -// This file is automatically updated during development when running `dev.ts`. - -import * as $index from "./routes/index.tsx"; - -const manifest = { - routes: { - "./routes/index.tsx": $index, - }, - islands: {}, - baseUrl: import.meta.url, -}; - -export default manifest; diff --git a/tests/fixture_invalid_handlers/main.ts b/tests/fixture_invalid_handlers/main.ts deleted file mode 100644 index 98cd1afab40..00000000000 --- a/tests/fixture_invalid_handlers/main.ts +++ /dev/null @@ -1,11 +0,0 @@ -/// -/// -/// -/// -/// - -import { start } from "$fresh/server.ts"; -import manifest from "./fresh.gen.ts"; - -// @ts-expect-error: the index.tsx file declares a "handlers" but no "handler", to simulate a typo or confusion on the user's part -await start(manifest); diff --git a/tests/fixture_invalid_handlers/routes/index.tsx b/tests/fixture_invalid_handlers/routes/index.tsx deleted file mode 100644 index 385b9164f82..00000000000 --- a/tests/fixture_invalid_handlers/routes/index.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import { Handlers } from "../../../server.ts"; - -export const handlers: Handlers = { - GET() { - throw new Error("FAIL"); - }, -}; diff --git a/tests/fixture_island_groups/routes/foo/(_islands)/Foo.tsx b/tests/fixture_island_groups/routes/foo/(_islands)/Foo.tsx new file mode 100644 index 00000000000..d5b5a360263 --- /dev/null +++ b/tests/fixture_island_groups/routes/foo/(_islands)/Foo.tsx @@ -0,0 +1,15 @@ +import { useSignal } from "@preact/signals"; +import { useEffect } from "preact/hooks"; + +export function Foo() { + const active = useSignal(false); + useEffect(() => { + active.value = true; + }, []); + + return ( +
+ {active.value ? "it works" : "it doesn't work"} +
+ ); +} diff --git a/tests/fixture_npm/routes/index.tsx b/tests/fixture_island_groups/routes/foo/index.tsx similarity index 55% rename from tests/fixture_npm/routes/index.tsx rename to tests/fixture_island_groups/routes/foo/index.tsx index 756b29807c7..7c245eecf7c 100644 --- a/tests/fixture_npm/routes/index.tsx +++ b/tests/fixture_island_groups/routes/foo/index.tsx @@ -1,9 +1,9 @@ -import Test from "../islands/Test.tsx"; +import { Foo } from "./(_islands)/Foo.tsx"; export default function Home() { return (
- +
); } diff --git a/tests/fixture_build_static/routes/index.tsx b/tests/fixture_island_groups/routes/index.tsx similarity index 53% rename from tests/fixture_build_static/routes/index.tsx rename to tests/fixture_island_groups/routes/index.tsx index de8f3841572..3d1c74845f3 100644 --- a/tests/fixture_build_static/routes/index.tsx +++ b/tests/fixture_island_groups/routes/index.tsx @@ -1,3 +1,3 @@ export default function Home() { - return

text

; + return

hello world

; } diff --git a/tests/fixture_island_nesting/deno.json b/tests/fixture_island_nesting/deno.json deleted file mode 100644 index a4dcfbf7951..00000000000 --- a/tests/fixture_island_nesting/deno.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "lock": false, - "tasks": { - "start": "deno run -A --watch=static/,routes/ dev.ts" - }, - "imports": { - "$fresh/": "../../", - "preact": "https://esm.sh/preact@10.22.0", - "preact/": "https://esm.sh/preact@10.22.0/", - "@preact/signals": "https://esm.sh/*@preact/signals@1.1.3", - "@preact/signals-core": "https://esm.sh/@preact/signals-core@1.2.3" - }, - "compilerOptions": { - "jsx": "react-jsx", - "jsxImportSource": "preact" - } -} diff --git a/tests/fixture_island_nesting/dev.ts b/tests/fixture_island_nesting/dev.ts deleted file mode 100755 index 2d85d6c183c..00000000000 --- a/tests/fixture_island_nesting/dev.ts +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env -S deno run -A --watch=static/,routes/ - -import dev from "$fresh/dev.ts"; - -await dev(import.meta.url, "./main.ts"); diff --git a/tests/fixture_island_nesting/fresh.gen.ts b/tests/fixture_island_nesting/fresh.gen.ts deleted file mode 100644 index 6eee68a01b7..00000000000 --- a/tests/fixture_island_nesting/fresh.gen.ts +++ /dev/null @@ -1,76 +0,0 @@ -// DO NOT EDIT. This file is generated by Fresh. -// This file SHOULD be checked into source version control. -// This file is automatically updated during development when running `dev.ts`. - -import * as $dropdown from "./routes/dropdown.tsx"; -import * as $index from "./routes/index.tsx"; -import * as $island_conditional from "./routes/island_conditional.tsx"; -import * as $island_conditional_lazy from "./routes/island_conditional_lazy.tsx"; -import * as $island_conditional_lazy_island from "./routes/island_conditional_lazy_island.tsx"; -import * as $island_fn_child from "./routes/island_fn_child.tsx"; -import * as $island_in_island from "./routes/island_in_island.tsx"; -import * as $island_in_island_definition from "./routes/island_in_island_definition.tsx"; -import * as $island_invalid_children from "./routes/island_invalid_children.tsx"; -import * as $island_invalid_children_fn from "./routes/island_invalid_children_fn.tsx"; -import * as $island_jsx_child from "./routes/island_jsx_child.tsx"; -import * as $island_jsx_children from "./routes/island_jsx_children.tsx"; -import * as $island_jsx_island_jsx from "./routes/island_jsx_island_jsx.tsx"; -import * as $island_jsx_text from "./routes/island_jsx_text.tsx"; -import * as $island_nested_props from "./routes/island_nested_props.tsx"; -import * as $island_order from "./routes/island_order.tsx"; -import * as $island_siblings from "./routes/island_siblings.tsx"; -import * as $island_valid_children from "./routes/island_valid_children.tsx"; -import * as $BooleanButton from "./islands/BooleanButton.tsx"; -import * as $Counter from "./islands/Counter.tsx"; -import * as $Dropdown from "./islands/Dropdown.tsx"; -import * as $FragmentIsland from "./islands/FragmentIsland.tsx"; -import * as $Island from "./islands/Island.tsx"; -import * as $IslandCenter from "./islands/IslandCenter.tsx"; -import * as $IslandConditional from "./islands/IslandConditional.tsx"; -import * as $IslandFn from "./islands/IslandFn.tsx"; -import * as $IslandInsideIsland from "./islands/IslandInsideIsland.tsx"; -import * as $IslandWithProps from "./islands/IslandWithProps.tsx"; -import * as $PassThrough from "./islands/PassThrough.tsx"; -import * as $ReadyMarker from "./islands/ReadyMarker.tsx"; -import { type Manifest } from "$fresh/server.ts"; - -const manifest = { - routes: { - "./routes/dropdown.tsx": $dropdown, - "./routes/index.tsx": $index, - "./routes/island_conditional.tsx": $island_conditional, - "./routes/island_conditional_lazy.tsx": $island_conditional_lazy, - "./routes/island_conditional_lazy_island.tsx": - $island_conditional_lazy_island, - "./routes/island_fn_child.tsx": $island_fn_child, - "./routes/island_in_island.tsx": $island_in_island, - "./routes/island_in_island_definition.tsx": $island_in_island_definition, - "./routes/island_invalid_children.tsx": $island_invalid_children, - "./routes/island_invalid_children_fn.tsx": $island_invalid_children_fn, - "./routes/island_jsx_child.tsx": $island_jsx_child, - "./routes/island_jsx_children.tsx": $island_jsx_children, - "./routes/island_jsx_island_jsx.tsx": $island_jsx_island_jsx, - "./routes/island_jsx_text.tsx": $island_jsx_text, - "./routes/island_nested_props.tsx": $island_nested_props, - "./routes/island_order.tsx": $island_order, - "./routes/island_siblings.tsx": $island_siblings, - "./routes/island_valid_children.tsx": $island_valid_children, - }, - islands: { - "./islands/BooleanButton.tsx": $BooleanButton, - "./islands/Counter.tsx": $Counter, - "./islands/Dropdown.tsx": $Dropdown, - "./islands/FragmentIsland.tsx": $FragmentIsland, - "./islands/Island.tsx": $Island, - "./islands/IslandCenter.tsx": $IslandCenter, - "./islands/IslandConditional.tsx": $IslandConditional, - "./islands/IslandFn.tsx": $IslandFn, - "./islands/IslandInsideIsland.tsx": $IslandInsideIsland, - "./islands/IslandWithProps.tsx": $IslandWithProps, - "./islands/PassThrough.tsx": $PassThrough, - "./islands/ReadyMarker.tsx": $ReadyMarker, - }, - baseUrl: import.meta.url, -} satisfies Manifest; - -export default manifest; diff --git a/tests/fixture_island_nesting/islands/BooleanButton.tsx b/tests/fixture_island_nesting/islands/BooleanButton.tsx deleted file mode 100644 index 33ddd6d7362..00000000000 --- a/tests/fixture_island_nesting/islands/BooleanButton.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { Signal } from "@preact/signals"; - -export default function BooleanButton({ signal }: { signal: Signal }) { - return ( - - ); -} diff --git a/tests/fixture_island_nesting/islands/Counter.tsx b/tests/fixture_island_nesting/islands/Counter.tsx deleted file mode 100644 index 45d262880f6..00000000000 --- a/tests/fixture_island_nesting/islands/Counter.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { Signal } from "@preact/signals"; - -export default function Counter({ count }: { count: Signal }) { - return ( -
-

{count}

- -
- ); -} diff --git a/tests/fixture_island_nesting/islands/Dropdown.tsx b/tests/fixture_island_nesting/islands/Dropdown.tsx deleted file mode 100644 index 8b096928259..00000000000 --- a/tests/fixture_island_nesting/islands/Dropdown.tsx +++ /dev/null @@ -1,51 +0,0 @@ -// islands/Dropdown.tsx -import { ComponentChildren, createContext } from "preact"; -import { useState } from "preact/hooks"; - -const DropdownContext = createContext<[boolean, (prev: boolean) => boolean]>( - [false, () => false], -); - -export default function Dropdown( - { children }: { children: ComponentChildren }, -) { - return ( - // deno-lint-ignore no-explicit-any - - {children} - - ); -} - -export function DropdownHandle( - { children }: { children: ComponentChildren }, -) { - return ( - - {([isMenuOpen, setIsMenuOpen]) => { - return ( - - ); - }} - - ); -} - -export function DropdownMenu({ children }: { children: ComponentChildren }) { - return ( - - {([isMenuOpen]) => { - if (isMenuOpen) { - return children; - } - return null; - }} - - ); -} diff --git a/tests/fixture_island_nesting/islands/Island.tsx b/tests/fixture_island_nesting/islands/Island.tsx deleted file mode 100644 index 1887f5af380..00000000000 --- a/tests/fixture_island_nesting/islands/Island.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { ComponentChildren } from "preact"; - -export default function Island(props: { children?: ComponentChildren }) { - return ( -
- {props.children} -
- ); -} diff --git a/tests/fixture_island_nesting/islands/IslandCenter.tsx b/tests/fixture_island_nesting/islands/IslandCenter.tsx deleted file mode 100644 index f01b83a261c..00000000000 --- a/tests/fixture_island_nesting/islands/IslandCenter.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export default function IslandCenter() { - return

center

; -} diff --git a/tests/fixture_island_nesting/islands/IslandConditional.tsx b/tests/fixture_island_nesting/islands/IslandConditional.tsx deleted file mode 100644 index 0f35429b39e..00000000000 --- a/tests/fixture_island_nesting/islands/IslandConditional.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { Signal } from "@preact/signals"; -import { ComponentChildren } from "preact"; - -export interface IslandConditionalProps { - show: Signal; - children?: ComponentChildren; -} - -export default function IslandConditional( - { show, children }: IslandConditionalProps, -) { - return ( -
- {show.value ?

island content

: <>{children}} -
- ); -} diff --git a/tests/fixture_island_nesting/islands/IslandFn.tsx b/tests/fixture_island_nesting/islands/IslandFn.tsx deleted file mode 100644 index 792ba4b87b0..00000000000 --- a/tests/fixture_island_nesting/islands/IslandFn.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { VNode } from "preact"; - -import FragmentIsland from "./FragmentIsland.tsx"; - -function Foo(props: { children: () => VNode }) { - return props.children(); -} - -export default function IslandFn() { - return ( -
- - {() => } - -
- ); -} diff --git a/tests/fixture_island_nesting/islands/IslandInsideIsland.tsx b/tests/fixture_island_nesting/islands/IslandInsideIsland.tsx deleted file mode 100644 index 03448481758..00000000000 --- a/tests/fixture_island_nesting/islands/IslandInsideIsland.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { ComponentChildren } from "preact"; -import Island from "./Island.tsx"; - -export default function IslandInsideIsland( - props: { children?: ComponentChildren }, -) { - return ( -
- - {props.children} - -
- ); -} diff --git a/tests/fixture_island_nesting/islands/IslandWithProps.tsx b/tests/fixture_island_nesting/islands/IslandWithProps.tsx deleted file mode 100644 index 035a6ab5fda..00000000000 --- a/tests/fixture_island_nesting/islands/IslandWithProps.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { useEffect, useState } from "preact/hooks"; - -export default function IslandWithProps( - props: { foo: { bar: string } }, -) { - const [showText, setShowText] = useState(false); - - useEffect(() => { - setShowText(true); - }, []); - - return ( -
-

- {showText ? props.foo.bar : "it doesn't work"} -

-
- ); -} diff --git a/tests/fixture_island_nesting/islands/PassThrough.tsx b/tests/fixture_island_nesting/islands/PassThrough.tsx deleted file mode 100644 index b9417dccfbc..00000000000 --- a/tests/fixture_island_nesting/islands/PassThrough.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { ComponentChildren } from "preact"; - -export function PassThrough(props: { children: ComponentChildren }) { - return
{props.children}
; -} diff --git a/tests/fixture_island_nesting/islands/ReadyMarker.tsx b/tests/fixture_island_nesting/islands/ReadyMarker.tsx deleted file mode 100644 index d3ef969c299..00000000000 --- a/tests/fixture_island_nesting/islands/ReadyMarker.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { useSignal } from "@preact/signals"; -import { useEffect } from "preact/hooks"; - -export function ReadyMarker() { - const sig = useSignal(false); - useEffect(() => { - sig.value = true; - }, []); - - return ( -

- {sig.value ? "mounted" : "pending"} -

- ); -} diff --git a/tests/fixture_island_nesting/main.ts b/tests/fixture_island_nesting/main.ts deleted file mode 100644 index dedce9cbb04..00000000000 --- a/tests/fixture_island_nesting/main.ts +++ /dev/null @@ -1,10 +0,0 @@ -/// -/// -/// -/// -/// - -import { start } from "$fresh/server.ts"; -import manifest from "./fresh.gen.ts"; - -await start(manifest); diff --git a/tests/fixture_island_nesting/main_wasm.ts b/tests/fixture_island_nesting/main_wasm.ts deleted file mode 100644 index 3e76084f418..00000000000 --- a/tests/fixture_island_nesting/main_wasm.ts +++ /dev/null @@ -1,13 +0,0 @@ -// Simulate Deno Deploy environment - -/// -/// -/// -/// -/// - -import "./polyfill_deno_deploy.ts"; -import { start } from "$fresh/server.ts"; -import manifest from "./fresh.gen.ts"; - -await start(manifest); diff --git a/tests/fixture_island_nesting/polyfill_deno_deploy.ts b/tests/fixture_island_nesting/polyfill_deno_deploy.ts deleted file mode 100644 index 73380028741..00000000000 --- a/tests/fixture_island_nesting/polyfill_deno_deploy.ts +++ /dev/null @@ -1,5 +0,0 @@ -// Simulate Deno Deploy environment - -//@ts-ignore: Remove Deno.run for simulating deno deploy env -// deno-lint-ignore no-deprecated-deno-api -delete Deno.run; diff --git a/tests/fixture_island_nesting/routes/dropdown.tsx b/tests/fixture_island_nesting/routes/dropdown.tsx deleted file mode 100644 index a11b4716336..00000000000 --- a/tests/fixture_island_nesting/routes/dropdown.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import Dropdown, { - DropdownHandle, - DropdownMenu, -} from "../islands/Dropdown.tsx"; - -export default function Page() { - return ( -
-

Dropdown

- - Click me! - -

Hello Menu!

-
-
-
- ); -} diff --git a/tests/fixture_island_nesting/routes/index.tsx b/tests/fixture_island_nesting/routes/index.tsx deleted file mode 100644 index 365c5ea9ad6..00000000000 --- a/tests/fixture_island_nesting/routes/index.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { basename, dirname, extname, fromFileUrl } from "../../deps.ts"; - -const __dirname = dirname(fromFileUrl(import.meta.url)); - -const links: string[] = []; -for (const file of Deno.readDirSync(__dirname)) { - if (file.name.startsWith("index")) continue; - const name = basename(file.name, extname(file.name)); - links.push(name); -} - -export default function Home() { - return ( -
-

Tests

-
    - {links.map((link) => { - return ( -
  • - {link} -
  • - ); - })} -
-
- ); -} diff --git a/tests/fixture_island_nesting/routes/island_conditional.tsx b/tests/fixture_island_nesting/routes/island_conditional.tsx deleted file mode 100644 index 98cb4b4fe26..00000000000 --- a/tests/fixture_island_nesting/routes/island_conditional.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import IslandConditional from "../islands/IslandConditional.tsx"; -import BooleanButton from "../islands/BooleanButton.tsx"; -import { signal } from "@preact/signals"; - -const show = signal(false); - -export default function Page() { - return ( -
- - -
- ); -} diff --git a/tests/fixture_island_nesting/routes/island_conditional_lazy.tsx b/tests/fixture_island_nesting/routes/island_conditional_lazy.tsx deleted file mode 100644 index 7d05cdbb704..00000000000 --- a/tests/fixture_island_nesting/routes/island_conditional_lazy.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import IslandConditional from "../islands/IslandConditional.tsx"; -import BooleanButton from "../islands/BooleanButton.tsx"; -import { signal } from "@preact/signals"; - -const show = signal(true); - -export default function Page() { - return ( -
- -

server rendered

-
- -
- ); -} diff --git a/tests/fixture_island_nesting/routes/island_conditional_lazy_island.tsx b/tests/fixture_island_nesting/routes/island_conditional_lazy_island.tsx deleted file mode 100644 index fae9ce6a92d..00000000000 --- a/tests/fixture_island_nesting/routes/island_conditional_lazy_island.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import IslandConditional from "../islands/IslandConditional.tsx"; -import BooleanButton from "../islands/BooleanButton.tsx"; -import { useSignal } from "@preact/signals"; -import Counter from "../islands/Counter.tsx"; -import { ReadyMarker } from "../islands/ReadyMarker.tsx"; - -export default function Page() { - const show = useSignal(true); - const count = useSignal(0); - - return ( -
- -
-

server rendered

- -
-
- - -
- ); -} diff --git a/tests/fixture_island_nesting/routes/island_fn_child.tsx b/tests/fixture_island_nesting/routes/island_fn_child.tsx deleted file mode 100644 index fcf8f42a890..00000000000 --- a/tests/fixture_island_nesting/routes/island_fn_child.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import IslandFn from "../islands/IslandFn.tsx"; - -export default function Home() { - return ( -
- -
- ); -} diff --git a/tests/fixture_island_nesting/routes/island_in_island.tsx b/tests/fixture_island_nesting/routes/island_in_island.tsx deleted file mode 100644 index e7b42610c0b..00000000000 --- a/tests/fixture_island_nesting/routes/island_in_island.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import Island from "../islands/Island.tsx"; - -export default function Home() { - return ( -
- - -

it works

-
-
-
- ); -} diff --git a/tests/fixture_island_nesting/routes/island_in_island_definition.tsx b/tests/fixture_island_nesting/routes/island_in_island_definition.tsx deleted file mode 100644 index 9e6ebd27bd3..00000000000 --- a/tests/fixture_island_nesting/routes/island_in_island_definition.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import IslandInsideIsland from "../islands/IslandInsideIsland.tsx"; - -export default function Home() { - return ( -
- -

it works

-
-
- ); -} diff --git a/tests/fixture_island_nesting/routes/island_invalid_children.tsx b/tests/fixture_island_nesting/routes/island_invalid_children.tsx deleted file mode 100644 index 3e413e77812..00000000000 --- a/tests/fixture_island_nesting/routes/island_invalid_children.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { PassThrough } from "../islands/PassThrough.tsx"; - -export default function Page() { - return ( -
- {{ foo: 123 }} -
- ); -} diff --git a/tests/fixture_island_nesting/routes/island_invalid_children_fn.tsx b/tests/fixture_island_nesting/routes/island_invalid_children_fn.tsx deleted file mode 100644 index 7f5936edf42..00000000000 --- a/tests/fixture_island_nesting/routes/island_invalid_children_fn.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { PassThrough } from "../islands/PassThrough.tsx"; - -export default function Page() { - return ( -
- {() => {}} -
- ); -} diff --git a/tests/fixture_island_nesting/routes/island_jsx_child.tsx b/tests/fixture_island_nesting/routes/island_jsx_child.tsx deleted file mode 100644 index 0fe5826031d..00000000000 --- a/tests/fixture_island_nesting/routes/island_jsx_child.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import Island from "../islands/Island.tsx"; - -export default function Home() { - return ( -
- -

it works

-
-
- ); -} diff --git a/tests/fixture_island_nesting/routes/island_jsx_children.tsx b/tests/fixture_island_nesting/routes/island_jsx_children.tsx deleted file mode 100644 index fa416fe823c..00000000000 --- a/tests/fixture_island_nesting/routes/island_jsx_children.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import Island from "../islands/Island.tsx"; - -export default function Home() { - return ( -
- -

it{" "}

-

works

-
-
- ); -} diff --git a/tests/fixture_island_nesting/routes/island_jsx_island_jsx.tsx b/tests/fixture_island_nesting/routes/island_jsx_island_jsx.tsx deleted file mode 100644 index dc944acddd7..00000000000 --- a/tests/fixture_island_nesting/routes/island_jsx_island_jsx.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { ComponentChildren } from "preact"; -import Island from "../islands/Island.tsx"; - -function Foo(props: { children?: ComponentChildren }) { - return ( -
- {props.children} -
- ); -} - -export default function Home() { - return ( -
- - - - -

it works

-
-
-
-
-
- ); -} diff --git a/tests/fixture_island_nesting/routes/island_jsx_text.tsx b/tests/fixture_island_nesting/routes/island_jsx_text.tsx deleted file mode 100644 index 93765af7fa9..00000000000 --- a/tests/fixture_island_nesting/routes/island_jsx_text.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import Island from "../islands/Island.tsx"; - -export default function Home() { - return ( -
- - {"it"} {"works"} - -
- ); -} diff --git a/tests/fixture_island_nesting/routes/island_nested_props.tsx b/tests/fixture_island_nesting/routes/island_nested_props.tsx deleted file mode 100644 index 82f5b9965cb..00000000000 --- a/tests/fixture_island_nesting/routes/island_nested_props.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import IslandWithProps from "../islands/IslandWithProps.tsx"; -import Island from "../islands/Island.tsx"; - -export default function Home() { - return ( -
- - - -
- ); -} diff --git a/tests/fixture_island_nesting/routes/island_order.tsx b/tests/fixture_island_nesting/routes/island_order.tsx deleted file mode 100644 index 34f4e2315ef..00000000000 --- a/tests/fixture_island_nesting/routes/island_order.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import IslandCenter from "../islands/IslandCenter.tsx"; - -export default function IslandOrder() { - return ( -
-

left

- -

right

-
- ); -} diff --git a/tests/fixture_island_nesting/routes/island_siblings.tsx b/tests/fixture_island_nesting/routes/island_siblings.tsx deleted file mode 100644 index 4deea04f9d5..00000000000 --- a/tests/fixture_island_nesting/routes/island_siblings.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import Island from "../islands/Island.tsx"; - -export default function Home() { - return ( -
- -

it works

-
- -

it works

-
-
- ); -} diff --git a/tests/fixture_island_nesting/routes/island_valid_children.tsx b/tests/fixture_island_nesting/routes/island_valid_children.tsx deleted file mode 100644 index 735007a5e2e..00000000000 --- a/tests/fixture_island_nesting/routes/island_valid_children.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { PassThrough } from "../islands/PassThrough.tsx"; - -function Foo() { - return

foo

; -} - -export default function Page() { - return ( -
- hello - {2} - {null} - {true} - {false} - {undefined} - -

hello

-
- - - -
- ); -} diff --git a/tests/fixture_islands_multiple/deno.json b/tests/fixture_islands_multiple/deno.json deleted file mode 100644 index e9246e0a88f..00000000000 --- a/tests/fixture_islands_multiple/deno.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "lock": false, - "imports": { - "$fresh/": "../../", - "preact": "https://esm.sh/preact@10.22.0", - "preact/": "https://esm.sh/preact@10.22.0/", - "@preact/signals": "https://esm.sh/*@preact/signals@1.2.2", - "@preact/signals-core": "https://esm.sh/@preact/signals-core@1.5.0" - }, - "compilerOptions": { - "jsx": "react-jsx", - "jsxImportSource": "preact" - } -} diff --git a/tests/fixture_islands_multiple/dev.ts b/tests/fixture_islands_multiple/dev.ts deleted file mode 100755 index 2d85d6c183c..00000000000 --- a/tests/fixture_islands_multiple/dev.ts +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env -S deno run -A --watch=static/,routes/ - -import dev from "$fresh/dev.ts"; - -await dev(import.meta.url, "./main.ts"); diff --git a/tests/fixture_islands_multiple/fresh.gen.ts b/tests/fixture_islands_multiple/fresh.gen.ts deleted file mode 100644 index 13c9b6db22b..00000000000 --- a/tests/fixture_islands_multiple/fresh.gen.ts +++ /dev/null @@ -1,23 +0,0 @@ -// DO NOT EDIT. This file is generated by Fresh. -// This file SHOULD be checked into source version control. -// This file is automatically updated during development when running `dev.ts`. - -import * as $index from "./routes/index.tsx"; -import * as $Multiple from "./islands/Multiple.tsx"; -import * as $MultipleDefault from "./islands/MultipleDefault.tsx"; -import * as $Single from "./islands/Single.tsx"; -import type { Manifest } from "$fresh/server.ts"; - -const manifest = { - routes: { - "./routes/index.tsx": $index, - }, - islands: { - "./islands/Multiple.tsx": $Multiple, - "./islands/MultipleDefault.tsx": $MultipleDefault, - "./islands/Single.tsx": $Single, - }, - baseUrl: import.meta.url, -} satisfies Manifest; - -export default manifest; diff --git a/tests/fixture_islands_multiple/islands/Multiple.tsx b/tests/fixture_islands_multiple/islands/Multiple.tsx deleted file mode 100644 index 495e757262d..00000000000 --- a/tests/fixture_islands_multiple/islands/Multiple.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { useSignal } from "@preact/signals"; - -export const thisShouldNotCauseProblems = 42; - -export function Multiple1() { - const sig = useSignal(0); - return ( -
-

Multiple1 Island: {sig}

- -
- ); -} - -export function Multiple2() { - const sig = useSignal(0); - return ( -
-

Multiple2 Island: {sig}

- -
- ); -} diff --git a/tests/fixture_islands_multiple/islands/MultipleDefault.tsx b/tests/fixture_islands_multiple/islands/MultipleDefault.tsx deleted file mode 100644 index e265bc0bd13..00000000000 --- a/tests/fixture_islands_multiple/islands/MultipleDefault.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { useSignal } from "@preact/signals"; - -export default function MultipleDefault() { - const sig = useSignal(0); - return ( -
-

MultipleDefault Island: {sig}

- -
- ); -} - -export function MultipleDefault1() { - const sig = useSignal(0); - return ( -
-

MultipleDefault1 Island: {sig}

- -
- ); -} - -export function MultipleDefault2() { - const sig = useSignal(0); - return ( -
-

MultipleDefault2 Island: {sig}

- -
- ); -} diff --git a/tests/fixture_islands_multiple/islands/Single.tsx b/tests/fixture_islands_multiple/islands/Single.tsx deleted file mode 100644 index f1c73354142..00000000000 --- a/tests/fixture_islands_multiple/islands/Single.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { useSignal } from "@preact/signals"; - -export default function Single() { - const sig = useSignal(0); - - return ( -
-

Single Island: {sig}

- -
- ); -} diff --git a/tests/fixture_islands_multiple/main.ts b/tests/fixture_islands_multiple/main.ts deleted file mode 100644 index dedce9cbb04..00000000000 --- a/tests/fixture_islands_multiple/main.ts +++ /dev/null @@ -1,10 +0,0 @@ -/// -/// -/// -/// -/// - -import { start } from "$fresh/server.ts"; -import manifest from "./fresh.gen.ts"; - -await start(manifest); diff --git a/tests/fixture_islands_multiple/routes/index.tsx b/tests/fixture_islands_multiple/routes/index.tsx deleted file mode 100644 index 2087f9d3f51..00000000000 --- a/tests/fixture_islands_multiple/routes/index.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import Single from "../islands/Single.tsx"; -import { Multiple1, Multiple2 } from "../islands/Multiple.tsx"; -import MultipleDefault, { - MultipleDefault1, - MultipleDefault2, -} from "../islands/MultipleDefault.tsx"; - -export default function Home() { - return ( -
-

Single

- -

Multiple

- - -

Multiple Default

- - - -
- ); -} diff --git a/tests/fixture_jsx_pragma/deno.json b/tests/fixture_jsx_pragma/deno.json deleted file mode 100644 index b60fbe5bdf0..00000000000 --- a/tests/fixture_jsx_pragma/deno.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "lock": false, - "imports": { - "$fresh/": "../../", - "preact": "https://esm.sh/preact@10.22.0", - "preact/": "https://esm.sh/preact@10.22.0/" - } -} diff --git a/tests/fixture_jsx_pragma/dev.ts b/tests/fixture_jsx_pragma/dev.ts deleted file mode 100755 index 2d85d6c183c..00000000000 --- a/tests/fixture_jsx_pragma/dev.ts +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env -S deno run -A --watch=static/,routes/ - -import dev from "$fresh/dev.ts"; - -await dev(import.meta.url, "./main.ts"); diff --git a/tests/fixture_jsx_pragma/fresh.gen.ts b/tests/fixture_jsx_pragma/fresh.gen.ts deleted file mode 100644 index da62588d9a4..00000000000 --- a/tests/fixture_jsx_pragma/fresh.gen.ts +++ /dev/null @@ -1,19 +0,0 @@ -// DO NOT EDIT. This file is generated by Fresh. -// This file SHOULD be checked into source version control. -// This file is automatically updated during development when running `dev.ts`. - -import * as $index from "./routes/index.tsx"; -import * as $Island from "./islands/Island.tsx"; -import { type Manifest } from "$fresh/server.ts"; - -const manifest = { - routes: { - "./routes/index.tsx": $index, - }, - islands: { - "./islands/Island.tsx": $Island, - }, - baseUrl: import.meta.url, -} satisfies Manifest; - -export default manifest; diff --git a/tests/fixture_jsx_pragma/islands/Island.tsx b/tests/fixture_jsx_pragma/islands/Island.tsx deleted file mode 100644 index e9efb6da932..00000000000 --- a/tests/fixture_jsx_pragma/islands/Island.tsx +++ /dev/null @@ -1,12 +0,0 @@ -/** @jsx h */ -import { h } from "preact"; -import { IS_BROWSER } from "../../../src/runtime/utils.ts"; - -export default function Island() { - const id = IS_BROWSER ? "csr" : "ssr"; - return ( -
-

{id}

-
- ); -} diff --git a/tests/fixture_jsx_pragma/main.ts b/tests/fixture_jsx_pragma/main.ts deleted file mode 100644 index 84a9d01dfa2..00000000000 --- a/tests/fixture_jsx_pragma/main.ts +++ /dev/null @@ -1,8 +0,0 @@ -/// -/// -/// -/// - -import { start } from "$fresh/server.ts"; -import manifest from "./fresh.gen.ts"; -await start(manifest); diff --git a/tests/fixture_jsx_pragma/routes/index.tsx b/tests/fixture_jsx_pragma/routes/index.tsx deleted file mode 100644 index 6f214c59b6e..00000000000 --- a/tests/fixture_jsx_pragma/routes/index.tsx +++ /dev/null @@ -1,12 +0,0 @@ -/** @jsx h */ -import { h } from "preact"; -import Island from "../islands/Island.tsx"; - -export default function Home() { - return ( -
-

Hello World

- -
- ); -} diff --git a/tests/fixture_jsx_precompile/deno.json b/tests/fixture_jsx_precompile/deno.json deleted file mode 100644 index 641df7dc2cd..00000000000 --- a/tests/fixture_jsx_precompile/deno.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "lock": false, - "compilerOptions": { - "jsx": "precompile", - "jsxImportSource": "preact" - }, - "imports": { - "$fresh/": "../../", - "preact": "https://esm.sh/preact@10.22.0", - "preact/": "https://esm.sh/preact@10.22.0/" - } -} diff --git a/tests/fixture_jsx_precompile/dev.ts b/tests/fixture_jsx_precompile/dev.ts deleted file mode 100755 index 2d85d6c183c..00000000000 --- a/tests/fixture_jsx_precompile/dev.ts +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env -S deno run -A --watch=static/,routes/ - -import dev from "$fresh/dev.ts"; - -await dev(import.meta.url, "./main.ts"); diff --git a/tests/fixture_jsx_precompile/fresh.gen.ts b/tests/fixture_jsx_precompile/fresh.gen.ts deleted file mode 100644 index a248647dc77..00000000000 --- a/tests/fixture_jsx_precompile/fresh.gen.ts +++ /dev/null @@ -1,21 +0,0 @@ -// DO NOT EDIT. This file is generated by Fresh. -// This file SHOULD be checked into source version control. -// This file is automatically updated during development when running `dev.ts`. - -import * as $head from "./routes/head.tsx"; -import * as $index from "./routes/index.tsx"; -import * as $Island from "./islands/Island.tsx"; -import { type Manifest } from "$fresh/server.ts"; - -const manifest = { - routes: { - "./routes/head.tsx": $head, - "./routes/index.tsx": $index, - }, - islands: { - "./islands/Island.tsx": $Island, - }, - baseUrl: import.meta.url, -} satisfies Manifest; - -export default manifest; diff --git a/tests/fixture_jsx_precompile/islands/Island.tsx b/tests/fixture_jsx_precompile/islands/Island.tsx deleted file mode 100644 index de0460a07ef..00000000000 --- a/tests/fixture_jsx_precompile/islands/Island.tsx +++ /dev/null @@ -1,7 +0,0 @@ -export default function Island() { - return ( -

- it works -

- ); -} diff --git a/tests/fixture_jsx_precompile/main.ts b/tests/fixture_jsx_precompile/main.ts deleted file mode 100644 index 84a9d01dfa2..00000000000 --- a/tests/fixture_jsx_precompile/main.ts +++ /dev/null @@ -1,8 +0,0 @@ -/// -/// -/// -/// - -import { start } from "$fresh/server.ts"; -import manifest from "./fresh.gen.ts"; -await start(manifest); diff --git a/tests/fixture_jsx_precompile/routes/head.tsx b/tests/fixture_jsx_precompile/routes/head.tsx deleted file mode 100644 index 08013a96267..00000000000 --- a/tests/fixture_jsx_precompile/routes/head.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { Head } from "$fresh/runtime.ts"; - -export default function Page() { - return ( -
- - foo - - -

Hello World

-
- ); -} diff --git a/tests/fixture_jsx_precompile/routes/index.tsx b/tests/fixture_jsx_precompile/routes/index.tsx deleted file mode 100644 index 116337c6fe8..00000000000 --- a/tests/fixture_jsx_precompile/routes/index.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import Island from "../islands/Island.tsx"; - -export default function Home() { - return ( -
-

Hello World

- -
- ); -} diff --git a/tests/fixture_layouts/dev.ts b/tests/fixture_layouts/dev.ts deleted file mode 100644 index 2d85d6c183c..00000000000 --- a/tests/fixture_layouts/dev.ts +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env -S deno run -A --watch=static/,routes/ - -import dev from "$fresh/dev.ts"; - -await dev(import.meta.url, "./main.ts"); diff --git a/tests/fixture_layouts/fresh.gen.ts b/tests/fixture_layouts/fresh.gen.ts deleted file mode 100644 index 6d8f81dd461..00000000000 --- a/tests/fixture_layouts/fresh.gen.ts +++ /dev/null @@ -1,83 +0,0 @@ -// DO NOT EDIT. This file is generated by Fresh. -// This file SHOULD be checked into source version control. -// This file is automatically updated during development when running `dev.ts`. - -import * as $_app from "./routes/_app.tsx"; -import * as $_layout from "./routes/_layout.tsx"; -import * as $_middleware from "./routes/_middleware.ts"; -import * as $async_layout from "./routes/async/_layout.tsx"; -import * as $async_index from "./routes/async/index.tsx"; -import * as $async_redirect_layout from "./routes/async/redirect/_layout.tsx"; -import * as $async_redirect_index from "./routes/async/redirect/index.tsx"; -import * as $async_sub_layout from "./routes/async/sub/_layout.tsx"; -import * as $async_sub_index from "./routes/async/sub/index.tsx"; -import * as $dynamic_tenant_index from "./routes/dynamic/[tenant]/index.tsx"; -import * as $files_js_layout from "./routes/files/js/_layout.js"; -import * as $files_js_index from "./routes/files/js/index.js"; -import * as $files_jsx_layout from "./routes/files/jsx/_layout.jsx"; -import * as $files_jsx_index from "./routes/files/jsx/index.jsx"; -import * as $files_ts_layout from "./routes/files/ts/_layout.ts"; -import * as $files_ts_index from "./routes/files/ts/index.ts"; -import * as $files_tsx_layout from "./routes/files/tsx/_layout.tsx"; -import * as $files_tsx_index from "./routes/files/tsx/index.tsx"; -import * as $foo_layout from "./routes/foo/_layout.tsx"; -import * as $foo_bar from "./routes/foo/bar.tsx"; -import * as $foo_index from "./routes/foo/index.tsx"; -import * as $index from "./routes/index.tsx"; -import * as $other from "./routes/other.tsx"; -import * as $override_layout from "./routes/override/_layout.tsx"; -import * as $override_index from "./routes/override/index.tsx"; -import * as $override_layout_no_app_layout from "./routes/override/layout_no_app/_layout.tsx"; -import * as $override_layout_no_app_index from "./routes/override/layout_no_app/index.tsx"; -import * as $override_no_app from "./routes/override/no_app.tsx"; -import * as $override_no_layout from "./routes/override/no_layout.tsx"; -import * as $override_no_layout_no_app from "./routes/override/no_layout_no_app.tsx"; -import * as $skip_sub_layout from "./routes/skip/sub/_layout.tsx"; -import * as $skip_sub_index from "./routes/skip/sub/index.tsx"; -import * as $dynamic_tenant_islands_Counter from "./routes/dynamic/[tenant]/(_islands)/Counter.tsx"; -import { type Manifest } from "$fresh/server.ts"; - -const manifest = { - routes: { - "./routes/_app.tsx": $_app, - "./routes/_layout.tsx": $_layout, - "./routes/_middleware.ts": $_middleware, - "./routes/async/_layout.tsx": $async_layout, - "./routes/async/index.tsx": $async_index, - "./routes/async/redirect/_layout.tsx": $async_redirect_layout, - "./routes/async/redirect/index.tsx": $async_redirect_index, - "./routes/async/sub/_layout.tsx": $async_sub_layout, - "./routes/async/sub/index.tsx": $async_sub_index, - "./routes/dynamic/[tenant]/index.tsx": $dynamic_tenant_index, - "./routes/files/js/_layout.js": $files_js_layout, - "./routes/files/js/index.js": $files_js_index, - "./routes/files/jsx/_layout.jsx": $files_jsx_layout, - "./routes/files/jsx/index.jsx": $files_jsx_index, - "./routes/files/ts/_layout.ts": $files_ts_layout, - "./routes/files/ts/index.ts": $files_ts_index, - "./routes/files/tsx/_layout.tsx": $files_tsx_layout, - "./routes/files/tsx/index.tsx": $files_tsx_index, - "./routes/foo/_layout.tsx": $foo_layout, - "./routes/foo/bar.tsx": $foo_bar, - "./routes/foo/index.tsx": $foo_index, - "./routes/index.tsx": $index, - "./routes/other.tsx": $other, - "./routes/override/_layout.tsx": $override_layout, - "./routes/override/index.tsx": $override_index, - "./routes/override/layout_no_app/_layout.tsx": - $override_layout_no_app_layout, - "./routes/override/layout_no_app/index.tsx": $override_layout_no_app_index, - "./routes/override/no_app.tsx": $override_no_app, - "./routes/override/no_layout.tsx": $override_no_layout, - "./routes/override/no_layout_no_app.tsx": $override_no_layout_no_app, - "./routes/skip/sub/_layout.tsx": $skip_sub_layout, - "./routes/skip/sub/index.tsx": $skip_sub_index, - }, - islands: { - "./routes/dynamic/[tenant]/(_islands)/Counter.tsx": - $dynamic_tenant_islands_Counter, - }, - baseUrl: import.meta.url, -} satisfies Manifest; - -export default manifest; diff --git a/tests/fixture_layouts/main.ts b/tests/fixture_layouts/main.ts deleted file mode 100644 index 0f15e8defa4..00000000000 --- a/tests/fixture_layouts/main.ts +++ /dev/null @@ -1,10 +0,0 @@ -/// -/// -/// -/// -/// - -import { start } from "$fresh/server.ts"; -import manifest from "./fresh.gen.ts"; - -await start(manifest); diff --git a/tests/fixture_layouts/routes/_app.tsx b/tests/fixture_layouts/routes/_app.tsx deleted file mode 100644 index dfacc1ff903..00000000000 --- a/tests/fixture_layouts/routes/_app.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { PageProps } from "$fresh/server.ts"; - -export default function App({ Component, state }: PageProps) { - return ( -
- -
- ); -} diff --git a/tests/fixture_layouts/routes/_layout.tsx b/tests/fixture_layouts/routes/_layout.tsx deleted file mode 100644 index 6f98ab60120..00000000000 --- a/tests/fixture_layouts/routes/_layout.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { PageProps } from "$fresh/server.ts"; -import { LayoutState } from "./_middleware.ts"; - -export default function RootLayout( - { Component, state }: PageProps, -) { - return ( -
- {state.something === "it works" ? "it works\n" : "it doesn't work\n"} - -
- ); -} diff --git a/tests/fixture_layouts/routes/_middleware.ts b/tests/fixture_layouts/routes/_middleware.ts deleted file mode 100644 index 7fa35adca90..00000000000 --- a/tests/fixture_layouts/routes/_middleware.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { FreshContext } from "$fresh/server.ts"; - -export type LayoutState = { - something: string; -}; - -export const handler = ( - _req: Request, - ctx: FreshContext, -) => { - ctx.state.something = "it works"; - return ctx.next(); -}; diff --git a/tests/fixture_layouts/routes/async/_layout.tsx b/tests/fixture_layouts/routes/async/_layout.tsx deleted file mode 100644 index 3f6a3c0cd16..00000000000 --- a/tests/fixture_layouts/routes/async/_layout.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { FreshContext } from "$fresh/server.ts"; -import { delay } from "$fresh/tests/deps.ts"; - -export default async function AsyncLayout( - req: Request, - ctx: FreshContext, -) { - await delay(10); - return ( -
-

Async layout

- -
- ); -} diff --git a/tests/fixture_layouts/routes/async/index.tsx b/tests/fixture_layouts/routes/async/index.tsx deleted file mode 100644 index 648f622281c..00000000000 --- a/tests/fixture_layouts/routes/async/index.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { RouteContext } from "$fresh/server.ts"; - -export default async function AsyncPage(req: Request, ctx: RouteContext) { - await new Promise((r) => setTimeout(r, 10)); - return ( -
-

Async page

-
- ); -} diff --git a/tests/fixture_layouts/routes/async/redirect/_layout.tsx b/tests/fixture_layouts/routes/async/redirect/_layout.tsx deleted file mode 100644 index 20bf91c4c46..00000000000 --- a/tests/fixture_layouts/routes/async/redirect/_layout.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { FreshContext } from "$fresh/server.ts"; -import { delay } from "$std/async/mod.ts"; - -export default async function AsyncSubLayout( - req: Request, - ctx: FreshContext, -) { - await delay(10); - return new Response(null, { - status: 307, - headers: { Location: "/async/sub" }, - }); -} diff --git a/tests/fixture_layouts/routes/async/redirect/index.tsx b/tests/fixture_layouts/routes/async/redirect/index.tsx deleted file mode 100644 index f3380619535..00000000000 --- a/tests/fixture_layouts/routes/async/redirect/index.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { RouteContext } from "$fresh/server.ts"; - -export default async function AsyncRedirectPage( - req: Request, - ctx: RouteContext, -) { - await new Promise((r) => setTimeout(r, 10)); - return ( -
-

Async Redirect page

-
- ); -} diff --git a/tests/fixture_layouts/routes/async/sub/_layout.tsx b/tests/fixture_layouts/routes/async/sub/_layout.tsx deleted file mode 100644 index 0c55cef87a8..00000000000 --- a/tests/fixture_layouts/routes/async/sub/_layout.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { FreshContext } from "$fresh/server.ts"; - -export default async function AsyncSubLayout( - req: Request, - ctx: FreshContext, -) { - await new Promise((r) => setTimeout(r, 10)); - return ( -
-

Async Sub layout

- -
- ); -} diff --git a/tests/fixture_layouts/routes/async/sub/index.tsx b/tests/fixture_layouts/routes/async/sub/index.tsx deleted file mode 100644 index c63bd1845ca..00000000000 --- a/tests/fixture_layouts/routes/async/sub/index.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { RouteContext } from "$fresh/server.ts"; - -export default async function AsyncSubPage(req: Request, ctx: RouteContext) { - await new Promise((r) => setTimeout(r, 10)); - return ( -
-

Async Sub page

-
- ); -} diff --git a/tests/fixture_layouts/routes/dynamic/[tenant]/(_islands)/Counter.tsx b/tests/fixture_layouts/routes/dynamic/[tenant]/(_islands)/Counter.tsx deleted file mode 100644 index 5f015b1ee1f..00000000000 --- a/tests/fixture_layouts/routes/dynamic/[tenant]/(_islands)/Counter.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import type { Signal } from "@preact/signals"; -import { IS_BROWSER } from "$fresh/runtime.ts"; - -interface CounterProps { - count: Signal; - id: string; -} - -export default function Counter(props: CounterProps) { - return ( -
-

{props.count}

- -
- ); -} diff --git a/tests/fixture_layouts/routes/dynamic/[tenant]/index.tsx b/tests/fixture_layouts/routes/dynamic/[tenant]/index.tsx deleted file mode 100644 index 8af146de305..00000000000 --- a/tests/fixture_layouts/routes/dynamic/[tenant]/index.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { useSignal } from "@preact/signals"; -import Counter from "./(_islands)/Counter.tsx"; - -export default function Home() { - return ( -
- -
- ); -} diff --git a/tests/fixture_layouts/routes/files/js/_layout.js b/tests/fixture_layouts/routes/files/js/_layout.js deleted file mode 100644 index 57fb09c0329..00000000000 --- a/tests/fixture_layouts/routes/files/js/_layout.js +++ /dev/null @@ -1,5 +0,0 @@ -import { h } from "preact"; - -export default function JsLayout({ Component }) { - return h("div", { class: "js-layout" }, h(Component, null)); -} diff --git a/tests/fixture_layouts/routes/files/js/index.js b/tests/fixture_layouts/routes/files/js/index.js deleted file mode 100644 index 4980f6ed630..00000000000 --- a/tests/fixture_layouts/routes/files/js/index.js +++ /dev/null @@ -1,5 +0,0 @@ -import { h } from "preact"; - -export default function JsPage() { - return h("div", { class: "js-page" }, "/files/js"); -} diff --git a/tests/fixture_layouts/routes/files/jsx/_layout.jsx b/tests/fixture_layouts/routes/files/jsx/_layout.jsx deleted file mode 100644 index a35d3879a8b..00000000000 --- a/tests/fixture_layouts/routes/files/jsx/_layout.jsx +++ /dev/null @@ -1,9 +0,0 @@ -import { h } from "preact"; - -export default function JsxLayout({ Component }) { - return ( -
- -
- ); -} diff --git a/tests/fixture_layouts/routes/files/jsx/index.jsx b/tests/fixture_layouts/routes/files/jsx/index.jsx deleted file mode 100644 index e44ea6dff71..00000000000 --- a/tests/fixture_layouts/routes/files/jsx/index.jsx +++ /dev/null @@ -1,3 +0,0 @@ -export default function JsxPage() { - return
/files/jsx page
; -} diff --git a/tests/fixture_layouts/routes/files/ts/_layout.ts b/tests/fixture_layouts/routes/files/ts/_layout.ts deleted file mode 100644 index b3914e440bb..00000000000 --- a/tests/fixture_layouts/routes/files/ts/_layout.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { h } from "preact"; -import { PageProps } from "$fresh/server.ts"; - -export default function TsLayout({ Component }: PageProps) { - return h("div", { class: "ts-layout" }, h(Component, null)); -} diff --git a/tests/fixture_layouts/routes/files/ts/index.ts b/tests/fixture_layouts/routes/files/ts/index.ts deleted file mode 100644 index 930cdc4130e..00000000000 --- a/tests/fixture_layouts/routes/files/ts/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { h } from "preact"; - -export default function TsPage() { - return h("div", { class: "ts-page" }, "/files/ts"); -} diff --git a/tests/fixture_layouts/routes/files/tsx/_layout.tsx b/tests/fixture_layouts/routes/files/tsx/_layout.tsx deleted file mode 100644 index 64aacfb937b..00000000000 --- a/tests/fixture_layouts/routes/files/tsx/_layout.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { h } from "preact"; -import { PageProps } from "$fresh/server.ts"; - -export default function TsxLayout({ Component }: PageProps) { - return ( -
- -
- ); -} diff --git a/tests/fixture_layouts/routes/files/tsx/index.tsx b/tests/fixture_layouts/routes/files/tsx/index.tsx deleted file mode 100644 index b61a2b6610f..00000000000 --- a/tests/fixture_layouts/routes/files/tsx/index.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export default function TsxPage() { - return
/files/tsx page
; -} diff --git a/tests/fixture_layouts/routes/foo/_layout.tsx b/tests/fixture_layouts/routes/foo/_layout.tsx deleted file mode 100644 index cc507685dff..00000000000 --- a/tests/fixture_layouts/routes/foo/_layout.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { PageProps } from "$fresh/server.ts"; - -export default function FooLayout({ Component }: PageProps) { - return ( -
- -
- ); -} diff --git a/tests/fixture_layouts/routes/foo/bar.tsx b/tests/fixture_layouts/routes/foo/bar.tsx deleted file mode 100644 index c5cc588e24d..00000000000 --- a/tests/fixture_layouts/routes/foo/bar.tsx +++ /dev/null @@ -1,7 +0,0 @@ -export default function BarPage() { - return ( -
- /foo/bar page -
- ); -} diff --git a/tests/fixture_layouts/routes/foo/index.tsx b/tests/fixture_layouts/routes/foo/index.tsx deleted file mode 100644 index b2db53f77ef..00000000000 --- a/tests/fixture_layouts/routes/foo/index.tsx +++ /dev/null @@ -1,7 +0,0 @@ -export default function FooPage() { - return ( -
- /foo index page -
- ); -} diff --git a/tests/fixture_layouts/routes/index.tsx b/tests/fixture_layouts/routes/index.tsx deleted file mode 100644 index 13e0de19c47..00000000000 --- a/tests/fixture_layouts/routes/index.tsx +++ /dev/null @@ -1,7 +0,0 @@ -export default function Home() { - return ( -
- Home -
- ); -} diff --git a/tests/fixture_layouts/routes/other.tsx b/tests/fixture_layouts/routes/other.tsx deleted file mode 100644 index 47d7befe327..00000000000 --- a/tests/fixture_layouts/routes/other.tsx +++ /dev/null @@ -1,7 +0,0 @@ -export default function OtherPage() { - return ( -
- /other -
- ); -} diff --git a/tests/fixture_layouts/routes/override/_layout.tsx b/tests/fixture_layouts/routes/override/_layout.tsx deleted file mode 100644 index d18a52a495f..00000000000 --- a/tests/fixture_layouts/routes/override/_layout.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { LayoutConfig, PageProps } from "$fresh/server.ts"; - -export const config: LayoutConfig = { - skipInheritedLayouts: true, -}; - -export default function OverrideLayout({ Component }: PageProps) { - return ( -
- -
- ); -} diff --git a/tests/fixture_layouts/routes/override/index.tsx b/tests/fixture_layouts/routes/override/index.tsx deleted file mode 100644 index e9514f4890e..00000000000 --- a/tests/fixture_layouts/routes/override/index.tsx +++ /dev/null @@ -1,7 +0,0 @@ -export default function OverridePage() { - return ( -
- /override index page -
- ); -} diff --git a/tests/fixture_layouts/routes/override/layout_no_app/_layout.tsx b/tests/fixture_layouts/routes/override/layout_no_app/_layout.tsx deleted file mode 100644 index d894f401d5a..00000000000 --- a/tests/fixture_layouts/routes/override/layout_no_app/_layout.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { LayoutConfig, PageProps } from "$fresh/server.ts"; - -export const config: LayoutConfig = { - skipAppWrapper: true, -}; - -export default function OverrideLayout({ Component }: PageProps) { - return ( -
- -
- ); -} diff --git a/tests/fixture_layouts/routes/override/layout_no_app/index.tsx b/tests/fixture_layouts/routes/override/layout_no_app/index.tsx deleted file mode 100644 index 20e6112bf0c..00000000000 --- a/tests/fixture_layouts/routes/override/layout_no_app/index.tsx +++ /dev/null @@ -1,7 +0,0 @@ -export default function OverridePage() { - return ( -
- /override/layout_no_app page -
- ); -} diff --git a/tests/fixture_layouts/routes/override/no_app.tsx b/tests/fixture_layouts/routes/override/no_app.tsx deleted file mode 100644 index 0cbd5d35f97..00000000000 --- a/tests/fixture_layouts/routes/override/no_app.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { RouteConfig } from "$fresh/server.ts"; - -export const config: RouteConfig = { - skipAppWrapper: true, -}; - -export default function OverridePage() { - return ( -

- no _app.tsx template -

- ); -} diff --git a/tests/fixture_layouts/routes/override/no_layout.tsx b/tests/fixture_layouts/routes/override/no_layout.tsx deleted file mode 100644 index ff6b1dc2399..00000000000 --- a/tests/fixture_layouts/routes/override/no_layout.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { LayoutConfig } from "$fresh/server.ts"; - -export const config: LayoutConfig = { - skipInheritedLayouts: true, -}; - -export default function OverridePage() { - return ( -

- no layouts -

- ); -} diff --git a/tests/fixture_layouts/routes/override/no_layout_no_app.tsx b/tests/fixture_layouts/routes/override/no_layout_no_app.tsx deleted file mode 100644 index e48d50fbf5b..00000000000 --- a/tests/fixture_layouts/routes/override/no_layout_no_app.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { LayoutConfig } from "$fresh/server.ts"; - -export const config: LayoutConfig = { - skipAppWrapper: true, - skipInheritedLayouts: true, -}; - -export default function OverridePage() { - return ( -

- no _app.tsx template and no layouts -

- ); -} diff --git a/tests/fixture_layouts/routes/skip/sub/_layout.tsx b/tests/fixture_layouts/routes/skip/sub/_layout.tsx deleted file mode 100644 index 9f5c6637459..00000000000 --- a/tests/fixture_layouts/routes/skip/sub/_layout.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { PageProps } from "$fresh/server.ts"; - -export default function SubLayout({ Component }: PageProps) { - return ( -
- -
- ); -} diff --git a/tests/fixture_layouts/routes/skip/sub/index.tsx b/tests/fixture_layouts/routes/skip/sub/index.tsx deleted file mode 100644 index 7c22ad9e513..00000000000 --- a/tests/fixture_layouts/routes/skip/sub/index.tsx +++ /dev/null @@ -1,7 +0,0 @@ -export default function SubPage() { - return ( -
- /skip/sub page -
- ); -} diff --git a/tests/fixture_layouts_2/dev.ts b/tests/fixture_layouts_2/dev.ts deleted file mode 100644 index 2d85d6c183c..00000000000 --- a/tests/fixture_layouts_2/dev.ts +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env -S deno run -A --watch=static/,routes/ - -import dev from "$fresh/dev.ts"; - -await dev(import.meta.url, "./main.ts"); diff --git a/tests/fixture_layouts_2/fresh.gen.ts b/tests/fixture_layouts_2/fresh.gen.ts deleted file mode 100644 index e902f3b07eb..00000000000 --- a/tests/fixture_layouts_2/fresh.gen.ts +++ /dev/null @@ -1,21 +0,0 @@ -// DO NOT EDIT. This file is generated by Fresh. -// This file SHOULD be checked into source version control. -// This file is automatically updated during development when running `dev.ts`. - -import * as $_app from "./routes/_app.tsx"; -import * as $_layout from "./routes/_layout.tsx"; -import * as $index from "./routes/index.tsx"; - -import { type Manifest } from "$fresh/server.ts"; - -const manifest = { - routes: { - "./routes/_app.tsx": $_app, - "./routes/_layout.tsx": $_layout, - "./routes/index.tsx": $index, - }, - islands: {}, - baseUrl: import.meta.url, -} satisfies Manifest; - -export default manifest; diff --git a/tests/fixture_layouts_2/main.ts b/tests/fixture_layouts_2/main.ts deleted file mode 100644 index 0f15e8defa4..00000000000 --- a/tests/fixture_layouts_2/main.ts +++ /dev/null @@ -1,10 +0,0 @@ -/// -/// -/// -/// -/// - -import { start } from "$fresh/server.ts"; -import manifest from "./fresh.gen.ts"; - -await start(manifest); diff --git a/tests/fixture_layouts_2/routes/_app.tsx b/tests/fixture_layouts_2/routes/_app.tsx deleted file mode 100644 index e749edd6061..00000000000 --- a/tests/fixture_layouts_2/routes/_app.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { defineApp } from "$fresh/server.ts"; - -export default defineApp((req, { Component }) => { - return ( -
- -
- ); -}); diff --git a/tests/fixture_layouts_2/routes/_layout.tsx b/tests/fixture_layouts_2/routes/_layout.tsx deleted file mode 100644 index 777702b6f30..00000000000 --- a/tests/fixture_layouts_2/routes/_layout.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { PageProps } from "$fresh/server.ts"; - -export default function RootLayout( - { Component }: PageProps, -) { - return ( -
- -
- ); -} diff --git a/tests/fixture_layouts_2/routes/index.tsx b/tests/fixture_layouts_2/routes/index.tsx deleted file mode 100644 index 13e0de19c47..00000000000 --- a/tests/fixture_layouts_2/routes/index.tsx +++ /dev/null @@ -1,7 +0,0 @@ -export default function Home() { - return ( -
- Home -
- ); -} diff --git a/tests/fixture_npm/deno.json b/tests/fixture_npm/deno.json deleted file mode 100644 index 388be4e441e..00000000000 --- a/tests/fixture_npm/deno.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "lock": false, - "imports": { - "$fresh/": "../../", - "preact": "https://esm.sh/preact@10.22.0", - "preact/": "https://esm.sh/preact@10.22.0/", - "@preact/signals": "https://esm.sh/*@preact/signals@1.1.3", - "@preact/signals-core": "https://esm.sh/@preact/signals-core@1.2.3" - }, - "compilerOptions": { - "jsx": "react-jsx", - "jsxImportSource": "preact" - } -} diff --git a/tests/fixture_npm/dev.ts b/tests/fixture_npm/dev.ts deleted file mode 100755 index 2d85d6c183c..00000000000 --- a/tests/fixture_npm/dev.ts +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env -S deno run -A --watch=static/,routes/ - -import dev from "$fresh/dev.ts"; - -await dev(import.meta.url, "./main.ts"); diff --git a/tests/fixture_npm/fresh.gen.ts b/tests/fixture_npm/fresh.gen.ts deleted file mode 100644 index 2f8ca9f7cc1..00000000000 --- a/tests/fixture_npm/fresh.gen.ts +++ /dev/null @@ -1,19 +0,0 @@ -// DO NOT EDIT. This file is generated by Fresh. -// This file SHOULD be checked into source version control. -// This file is automatically updated during development when running `dev.ts`. - -import * as $index from "./routes/index.tsx"; -import * as $Test from "./islands/Test.tsx"; -import { type Manifest } from "$fresh/server.ts"; - -const manifest = { - routes: { - "./routes/index.tsx": $index, - }, - islands: { - "./islands/Test.tsx": $Test, - }, - baseUrl: import.meta.url, -} satisfies Manifest; - -export default manifest; diff --git a/tests/fixture_npm/islands/Test.tsx b/tests/fixture_npm/islands/Test.tsx deleted file mode 100644 index eefe625adc6..00000000000 --- a/tests/fixture_npm/islands/Test.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import { IS_BROWSER } from "../../../runtime.ts"; -import isNumber from "npm:is-number"; - -export default function Test() { - let id = IS_BROWSER ? "browser" : "server"; - id += "-" + String(isNumber(1)); - return
{id}
; -} diff --git a/tests/fixture_npm/main.ts b/tests/fixture_npm/main.ts deleted file mode 100644 index dedce9cbb04..00000000000 --- a/tests/fixture_npm/main.ts +++ /dev/null @@ -1,10 +0,0 @@ -/// -/// -/// -/// -/// - -import { start } from "$fresh/server.ts"; -import manifest from "./fresh.gen.ts"; - -await start(manifest); diff --git a/tests/fixture_partials/components/Keyed.tsx b/tests/fixture_partials/components/Keyed.tsx deleted file mode 100644 index 4b20d8aefca..00000000000 --- a/tests/fixture_partials/components/Keyed.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { ComponentChildren } from "preact"; - -export function Keyed(props: { children?: ComponentChildren }) { - return <>{props.children}; -} diff --git a/tests/fixture_partials/deno.json b/tests/fixture_partials/deno.json deleted file mode 100644 index 21aa8f8eed1..00000000000 --- a/tests/fixture_partials/deno.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "lock": false, - "tasks": { - "start": "deno run -A --watch=static/,routes/ dev.ts" - }, - "imports": { - "$fresh/": "../../", - "twind": "https://esm.sh/twind@0.16.19", - "twind/": "https://esm.sh/twind@0.16.19/", - "preact": "https://esm.sh/preact@10.22.0", - "preact/": "https://esm.sh/preact@10.22.0/", - "preact-render-to-string": "https://esm.sh/*preact-render-to-string@6.1.0", - "@preact/signals": "https://esm.sh/*@preact/signals@1.1.3", - "@preact/signals-core": "https://esm.sh/@preact/signals-core@1.2.3" - }, - "compilerOptions": { - "jsx": "react-jsx", - "jsxImportSource": "preact" - } -} diff --git a/tests/fixture_partials/dev.ts b/tests/fixture_partials/dev.ts deleted file mode 100755 index 2d85d6c183c..00000000000 --- a/tests/fixture_partials/dev.ts +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env -S deno run -A --watch=static/,routes/ - -import dev from "$fresh/dev.ts"; - -await dev(import.meta.url, "./main.ts"); diff --git a/tests/fixture_partials/fresh.gen.ts b/tests/fixture_partials/fresh.gen.ts deleted file mode 100644 index ec5922e4450..00000000000 --- a/tests/fixture_partials/fresh.gen.ts +++ /dev/null @@ -1,292 +0,0 @@ -// DO NOT EDIT. This file is generated by Fresh. -// This file SHOULD be checked into source version control. -// This file is automatically updated during development when running `dev.ts`. - -import * as $_404 from "./routes/_404.tsx"; -import * as $_app from "./routes/_app.tsx"; -import * as $active_nav_foo_bar from "./routes/active_nav/foo/bar.tsx"; -import * as $active_nav_foo_index from "./routes/active_nav/foo/index.tsx"; -import * as $active_nav_index from "./routes/active_nav/index.tsx"; -import * as $active_nav_island from "./routes/active_nav/island.tsx"; -import * as $active_nav_partial_layout from "./routes/active_nav_partial/_layout.tsx"; -import * as $active_nav_partial_foo_bar from "./routes/active_nav_partial/foo/bar.tsx"; -import * as $active_nav_partial_foo_index from "./routes/active_nav_partial/foo/index.tsx"; -import * as $active_nav_partial_index from "./routes/active_nav_partial/index.tsx"; -import * as $active_nav_partial_island from "./routes/active_nav_partial/island.tsx"; -import * as $button_index from "./routes/button/index.tsx"; -import * as $button_update from "./routes/button/update.tsx"; -import * as $client_nav_layout from "./routes/client_nav/_layout.tsx"; -import * as $client_nav_index from "./routes/client_nav/index.tsx"; -import * as $client_nav_injected from "./routes/client_nav/injected.tsx"; -import * as $client_nav_page_a from "./routes/client_nav/page-a.tsx"; -import * as $client_nav_page_b from "./routes/client_nav/page-b.tsx"; -import * as $client_nav_page_c from "./routes/client_nav/page-c.tsx"; -import * as $client_nav_both_layout from "./routes/client_nav_both/_layout.tsx"; -import * as $client_nav_both_index from "./routes/client_nav_both/index.tsx"; -import * as $client_nav_both_page_a from "./routes/client_nav_both/page-a.tsx"; -import * as $client_nav_both_page_b from "./routes/client_nav_both/page-b.tsx"; -import * as $client_nav_both_page_c from "./routes/client_nav_both/page-c.tsx"; -import * as $client_nav_opt_out_layout from "./routes/client_nav_opt_out/_layout.tsx"; -import * as $client_nav_opt_out_index from "./routes/client_nav_opt_out/index.tsx"; -import * as $client_nav_opt_out_injected from "./routes/client_nav_opt_out/injected.tsx"; -import * as $client_nav_opt_out_page_a from "./routes/client_nav_opt_out/page-a.tsx"; -import * as $client_nav_opt_out_page_b from "./routes/client_nav_opt_out/page-b.tsx"; -import * as $client_nav_opt_out_page_c from "./routes/client_nav_opt_out/page-c.tsx"; -import * as $deep_partial_index from "./routes/deep_partial/index.tsx"; -import * as $deep_partial_injected from "./routes/deep_partial/injected.tsx"; -import * as $deep_partial_update from "./routes/deep_partial/update.tsx"; -import * as $duplicate_name_index from "./routes/duplicate_name/index.tsx"; -import * as $error_404 from "./routes/error_404.tsx"; -import * as $form_index from "./routes/form/index.tsx"; -import * as $form_injected from "./routes/form/injected.tsx"; -import * as $form_update from "./routes/form/update.tsx"; -import * as $form_get_index from "./routes/form_get/index.tsx"; -import * as $form_post_index from "./routes/form_post/index.tsx"; -import * as $form_submitter_index from "./routes/form_submitter/index.tsx"; -import * as $form_submitter_partial_index from "./routes/form_submitter_partial/index.tsx"; -import * as $form_submitter_partial_no_client_nav_index from "./routes/form_submitter_partial_no_client_nav/index.tsx"; -import * as $fragment_nav from "./routes/fragment_nav.tsx"; -import * as $fragment_nav_scroll from "./routes/fragment_nav_scroll.tsx"; -import * as $head_merge_duplicate from "./routes/head_merge/duplicate.tsx"; -import * as $head_merge_index from "./routes/head_merge/index.tsx"; -import * as $head_merge_injected from "./routes/head_merge/injected.tsx"; -import * as $head_merge_update from "./routes/head_merge/update.tsx"; -import * as $head_merge_without_title from "./routes/head_merge/without_title.tsx"; -import * as $index from "./routes/index.tsx"; -import * as $isPartial_middleware from "./routes/isPartial/_middleware.ts"; -import * as $isPartial_async from "./routes/isPartial/async.tsx"; -import * as $isPartial_handler from "./routes/isPartial/handler.tsx"; -import * as $isPartial_index from "./routes/isPartial/index.tsx"; -import * as $isPartial_injected from "./routes/isPartial/injected.tsx"; -import * as $island_instance_index from "./routes/island_instance/index.tsx"; -import * as $island_instance_injected from "./routes/island_instance/injected.tsx"; -import * as $island_instance_partial from "./routes/island_instance/partial.tsx"; -import * as $island_instance_partial_remove from "./routes/island_instance/partial_remove.tsx"; -import * as $island_instance_partial_replace from "./routes/island_instance/partial_replace.tsx"; -import * as $island_instance_multiple_index from "./routes/island_instance_multiple/index.tsx"; -import * as $island_instance_multiple_injected from "./routes/island_instance_multiple/injected.tsx"; -import * as $island_instance_multiple_partial from "./routes/island_instance_multiple/partial.tsx"; -import * as $island_instance_multiple_partial_both from "./routes/island_instance_multiple/partial_both.tsx"; -import * as $island_instance_nested_index from "./routes/island_instance_nested/index.tsx"; -import * as $island_instance_nested_injected from "./routes/island_instance_nested/injected.tsx"; -import * as $island_instance_nested_partial from "./routes/island_instance_nested/partial.tsx"; -import * as $island_instance_nested_replace from "./routes/island_instance_nested/replace.tsx"; -import * as $island_props_index from "./routes/island_props/index.tsx"; -import * as $island_props_injected from "./routes/island_props/injected.tsx"; -import * as $island_props_partial from "./routes/island_props/partial.tsx"; -import * as $island_props_signals_index from "./routes/island_props_signals/index.tsx"; -import * as $island_props_signals_injected from "./routes/island_props_signals/injected.tsx"; -import * as $island_props_signals_partial from "./routes/island_props_signals/partial.tsx"; -import * as $keys_index from "./routes/keys/index.tsx"; -import * as $keys_injected from "./routes/keys/injected.tsx"; -import * as $keys_swap from "./routes/keys/swap.tsx"; -import * as $keys_components_index from "./routes/keys_components/index.tsx"; -import * as $keys_components_injected from "./routes/keys_components/injected.tsx"; -import * as $keys_components_swap from "./routes/keys_components/swap.tsx"; -import * as $keys_confusion_index from "./routes/keys_confusion/index.tsx"; -import * as $keys_dom_index from "./routes/keys_dom/index.tsx"; -import * as $keys_dom_injected from "./routes/keys_dom/injected.tsx"; -import * as $keys_dom_swap from "./routes/keys_dom/swap.tsx"; -import * as $keys_outside_index from "./routes/keys_outside/index.tsx"; -import * as $loading_index from "./routes/loading/index.tsx"; -import * as $loading_injected from "./routes/loading/injected.tsx"; -import * as $loading_update from "./routes/loading/update.tsx"; -import * as $missing_partial_index from "./routes/missing_partial/index.tsx"; -import * as $missing_partial_injected from "./routes/missing_partial/injected.tsx"; -import * as $missing_partial_update from "./routes/missing_partial/update.tsx"; -import * as $mode_append from "./routes/mode/append.tsx"; -import * as $mode_index from "./routes/mode/index.tsx"; -import * as $mode_injected from "./routes/mode/injected.tsx"; -import * as $mode_prepend from "./routes/mode/prepend.tsx"; -import * as $mode_replace from "./routes/mode/replace.tsx"; -import * as $nested_index from "./routes/nested/index.tsx"; -import * as $nested_inner from "./routes/nested/inner.tsx"; -import * as $nested_outer from "./routes/nested/outer.tsx"; -import * as $no_islands_index from "./routes/no_islands/index.tsx"; -import * as $no_islands_injected from "./routes/no_islands/injected.tsx"; -import * as $no_islands_update from "./routes/no_islands/update.tsx"; -import * as $no_partial_response_index from "./routes/no_partial_response/index.tsx"; -import * as $no_partial_response_injected from "./routes/no_partial_response/injected.tsx"; -import * as $no_partial_response_update from "./routes/no_partial_response/update.tsx"; -import * as $partial_slot_inside_island from "./routes/partial_slot_inside_island.tsx"; -import * as $redirected_handler from "./routes/redirected/handler.tsx"; -import * as $redirected_index from "./routes/redirected/index.tsx"; -import * as $redirected_injected from "./routes/redirected/injected.tsx"; -import * as $redirected_redirect from "./routes/redirected/redirect.tsx"; -import * as $relative_link_index from "./routes/relative_link/index.tsx"; -import * as $scroll_restoration_index from "./routes/scroll_restoration/index.tsx"; -import * as $scroll_restoration_injected from "./routes/scroll_restoration/injected.tsx"; -import * as $scroll_restoration_update from "./routes/scroll_restoration/update.tsx"; -import * as $spoof_state_index from "./routes/spoof_state/index.tsx"; -import * as $spoof_state_partial from "./routes/spoof_state/partial.tsx"; -import * as $Counter from "./islands/Counter.tsx"; -import * as $CounterA from "./islands/CounterA.tsx"; -import * as $CounterB from "./islands/CounterB.tsx"; -import * as $DangerousIsland from "./islands/DangerousIsland.tsx"; -import * as $Fader from "./islands/Fader.tsx"; -import * as $InvalidSlot from "./islands/InvalidSlot.tsx"; -import * as $KeyExplorer from "./islands/KeyExplorer.tsx"; -import * as $LazyLink from "./islands/LazyLink.tsx"; -import * as $Logger from "./islands/Logger.tsx"; -import * as $Other from "./islands/Other.tsx"; -import * as $PartialTrigger from "./islands/PartialTrigger.tsx"; -import * as $PassThrough from "./islands/PassThrough.tsx"; -import * as $PropIsland from "./islands/PropIsland.tsx"; -import * as $SignalProp from "./islands/SignalProp.tsx"; -import * as $Spinner from "./islands/Spinner.tsx"; -import * as $Stateful from "./islands/Stateful.tsx"; -import { type Manifest } from "$fresh/server.ts"; - -const manifest = { - routes: { - "./routes/_404.tsx": $_404, - "./routes/_app.tsx": $_app, - "./routes/active_nav/foo/bar.tsx": $active_nav_foo_bar, - "./routes/active_nav/foo/index.tsx": $active_nav_foo_index, - "./routes/active_nav/index.tsx": $active_nav_index, - "./routes/active_nav/island.tsx": $active_nav_island, - "./routes/active_nav_partial/_layout.tsx": $active_nav_partial_layout, - "./routes/active_nav_partial/foo/bar.tsx": $active_nav_partial_foo_bar, - "./routes/active_nav_partial/foo/index.tsx": $active_nav_partial_foo_index, - "./routes/active_nav_partial/index.tsx": $active_nav_partial_index, - "./routes/active_nav_partial/island.tsx": $active_nav_partial_island, - "./routes/button/index.tsx": $button_index, - "./routes/button/update.tsx": $button_update, - "./routes/client_nav/_layout.tsx": $client_nav_layout, - "./routes/client_nav/index.tsx": $client_nav_index, - "./routes/client_nav/injected.tsx": $client_nav_injected, - "./routes/client_nav/page-a.tsx": $client_nav_page_a, - "./routes/client_nav/page-b.tsx": $client_nav_page_b, - "./routes/client_nav/page-c.tsx": $client_nav_page_c, - "./routes/client_nav_both/_layout.tsx": $client_nav_both_layout, - "./routes/client_nav_both/index.tsx": $client_nav_both_index, - "./routes/client_nav_both/page-a.tsx": $client_nav_both_page_a, - "./routes/client_nav_both/page-b.tsx": $client_nav_both_page_b, - "./routes/client_nav_both/page-c.tsx": $client_nav_both_page_c, - "./routes/client_nav_opt_out/_layout.tsx": $client_nav_opt_out_layout, - "./routes/client_nav_opt_out/index.tsx": $client_nav_opt_out_index, - "./routes/client_nav_opt_out/injected.tsx": $client_nav_opt_out_injected, - "./routes/client_nav_opt_out/page-a.tsx": $client_nav_opt_out_page_a, - "./routes/client_nav_opt_out/page-b.tsx": $client_nav_opt_out_page_b, - "./routes/client_nav_opt_out/page-c.tsx": $client_nav_opt_out_page_c, - "./routes/deep_partial/index.tsx": $deep_partial_index, - "./routes/deep_partial/injected.tsx": $deep_partial_injected, - "./routes/deep_partial/update.tsx": $deep_partial_update, - "./routes/duplicate_name/index.tsx": $duplicate_name_index, - "./routes/error_404.tsx": $error_404, - "./routes/form/index.tsx": $form_index, - "./routes/form/injected.tsx": $form_injected, - "./routes/form/update.tsx": $form_update, - "./routes/form_get/index.tsx": $form_get_index, - "./routes/form_post/index.tsx": $form_post_index, - "./routes/form_submitter/index.tsx": $form_submitter_index, - "./routes/form_submitter_partial/index.tsx": $form_submitter_partial_index, - "./routes/form_submitter_partial_no_client_nav/index.tsx": - $form_submitter_partial_no_client_nav_index, - "./routes/fragment_nav.tsx": $fragment_nav, - "./routes/fragment_nav_scroll.tsx": $fragment_nav_scroll, - "./routes/head_merge/duplicate.tsx": $head_merge_duplicate, - "./routes/head_merge/index.tsx": $head_merge_index, - "./routes/head_merge/injected.tsx": $head_merge_injected, - "./routes/head_merge/update.tsx": $head_merge_update, - "./routes/head_merge/without_title.tsx": $head_merge_without_title, - "./routes/index.tsx": $index, - "./routes/isPartial/_middleware.ts": $isPartial_middleware, - "./routes/isPartial/async.tsx": $isPartial_async, - "./routes/isPartial/handler.tsx": $isPartial_handler, - "./routes/isPartial/index.tsx": $isPartial_index, - "./routes/isPartial/injected.tsx": $isPartial_injected, - "./routes/island_instance/index.tsx": $island_instance_index, - "./routes/island_instance/injected.tsx": $island_instance_injected, - "./routes/island_instance/partial.tsx": $island_instance_partial, - "./routes/island_instance/partial_remove.tsx": - $island_instance_partial_remove, - "./routes/island_instance/partial_replace.tsx": - $island_instance_partial_replace, - "./routes/island_instance_multiple/index.tsx": - $island_instance_multiple_index, - "./routes/island_instance_multiple/injected.tsx": - $island_instance_multiple_injected, - "./routes/island_instance_multiple/partial.tsx": - $island_instance_multiple_partial, - "./routes/island_instance_multiple/partial_both.tsx": - $island_instance_multiple_partial_both, - "./routes/island_instance_nested/index.tsx": $island_instance_nested_index, - "./routes/island_instance_nested/injected.tsx": - $island_instance_nested_injected, - "./routes/island_instance_nested/partial.tsx": - $island_instance_nested_partial, - "./routes/island_instance_nested/replace.tsx": - $island_instance_nested_replace, - "./routes/island_props/index.tsx": $island_props_index, - "./routes/island_props/injected.tsx": $island_props_injected, - "./routes/island_props/partial.tsx": $island_props_partial, - "./routes/island_props_signals/index.tsx": $island_props_signals_index, - "./routes/island_props_signals/injected.tsx": - $island_props_signals_injected, - "./routes/island_props_signals/partial.tsx": $island_props_signals_partial, - "./routes/keys/index.tsx": $keys_index, - "./routes/keys/injected.tsx": $keys_injected, - "./routes/keys/swap.tsx": $keys_swap, - "./routes/keys_components/index.tsx": $keys_components_index, - "./routes/keys_components/injected.tsx": $keys_components_injected, - "./routes/keys_components/swap.tsx": $keys_components_swap, - "./routes/keys_confusion/index.tsx": $keys_confusion_index, - "./routes/keys_dom/index.tsx": $keys_dom_index, - "./routes/keys_dom/injected.tsx": $keys_dom_injected, - "./routes/keys_dom/swap.tsx": $keys_dom_swap, - "./routes/keys_outside/index.tsx": $keys_outside_index, - "./routes/loading/index.tsx": $loading_index, - "./routes/loading/injected.tsx": $loading_injected, - "./routes/loading/update.tsx": $loading_update, - "./routes/missing_partial/index.tsx": $missing_partial_index, - "./routes/missing_partial/injected.tsx": $missing_partial_injected, - "./routes/missing_partial/update.tsx": $missing_partial_update, - "./routes/mode/append.tsx": $mode_append, - "./routes/mode/index.tsx": $mode_index, - "./routes/mode/injected.tsx": $mode_injected, - "./routes/mode/prepend.tsx": $mode_prepend, - "./routes/mode/replace.tsx": $mode_replace, - "./routes/nested/index.tsx": $nested_index, - "./routes/nested/inner.tsx": $nested_inner, - "./routes/nested/outer.tsx": $nested_outer, - "./routes/no_islands/index.tsx": $no_islands_index, - "./routes/no_islands/injected.tsx": $no_islands_injected, - "./routes/no_islands/update.tsx": $no_islands_update, - "./routes/no_partial_response/index.tsx": $no_partial_response_index, - "./routes/no_partial_response/injected.tsx": $no_partial_response_injected, - "./routes/no_partial_response/update.tsx": $no_partial_response_update, - "./routes/partial_slot_inside_island.tsx": $partial_slot_inside_island, - "./routes/redirected/handler.tsx": $redirected_handler, - "./routes/redirected/index.tsx": $redirected_index, - "./routes/redirected/injected.tsx": $redirected_injected, - "./routes/redirected/redirect.tsx": $redirected_redirect, - "./routes/relative_link/index.tsx": $relative_link_index, - "./routes/scroll_restoration/index.tsx": $scroll_restoration_index, - "./routes/scroll_restoration/injected.tsx": $scroll_restoration_injected, - "./routes/scroll_restoration/update.tsx": $scroll_restoration_update, - "./routes/spoof_state/index.tsx": $spoof_state_index, - "./routes/spoof_state/partial.tsx": $spoof_state_partial, - }, - islands: { - "./islands/Counter.tsx": $Counter, - "./islands/CounterA.tsx": $CounterA, - "./islands/CounterB.tsx": $CounterB, - "./islands/DangerousIsland.tsx": $DangerousIsland, - "./islands/Fader.tsx": $Fader, - "./islands/InvalidSlot.tsx": $InvalidSlot, - "./islands/KeyExplorer.tsx": $KeyExplorer, - "./islands/LazyLink.tsx": $LazyLink, - "./islands/Logger.tsx": $Logger, - "./islands/Other.tsx": $Other, - "./islands/PartialTrigger.tsx": $PartialTrigger, - "./islands/PassThrough.tsx": $PassThrough, - "./islands/PropIsland.tsx": $PropIsland, - "./islands/SignalProp.tsx": $SignalProp, - "./islands/Spinner.tsx": $Spinner, - "./islands/Stateful.tsx": $Stateful, - }, - baseUrl: import.meta.url, -} satisfies Manifest; - -export default manifest; diff --git a/tests/fixture_partials/islands/Counter.tsx b/tests/fixture_partials/islands/Counter.tsx deleted file mode 100644 index 463560fb39c..00000000000 --- a/tests/fixture_partials/islands/Counter.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { useSignal } from "@preact/signals"; -import { Logger } from "./Logger.tsx"; - -export default function Counter(props: { id: string }) { - const sig = useSignal(0); - return ( - -
-

{sig.value}

- -
-
- ); -} diff --git a/tests/fixture_partials/islands/CounterA.tsx b/tests/fixture_partials/islands/CounterA.tsx deleted file mode 100644 index b4d905687cc..00000000000 --- a/tests/fixture_partials/islands/CounterA.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { useSignal } from "@preact/signals"; -import { Logger } from "./Logger.tsx"; - -export default function CounterA() { - const sig = useSignal(0); - return ( - -
-

{sig.value}

- -
-
- ); -} diff --git a/tests/fixture_partials/islands/CounterB.tsx b/tests/fixture_partials/islands/CounterB.tsx deleted file mode 100644 index f41df0212fb..00000000000 --- a/tests/fixture_partials/islands/CounterB.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { useSignal } from "@preact/signals"; -import { Logger } from "./Logger.tsx"; - -export default function CounterB() { - const sig = useSignal(0); - return ( - -
-

{sig.value}

- -
-
- ); -} diff --git a/tests/fixture_partials/islands/DangerousIsland.tsx b/tests/fixture_partials/islands/DangerousIsland.tsx deleted file mode 100644 index d2fd6cc6828..00000000000 --- a/tests/fixture_partials/islands/DangerousIsland.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { useEffect, useState } from "preact/hooks"; - -export default function DangerousIsland(props: { raw: string }) { - const [css, set] = useState(""); - useEffect(() => { - set("raw_ready"); - }, []); - - return
; -} diff --git a/tests/fixture_partials/islands/Fader.tsx b/tests/fixture_partials/islands/Fader.tsx deleted file mode 100644 index bbdfb69ae66..00000000000 --- a/tests/fixture_partials/islands/Fader.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { useEffect, useRef } from "preact/hooks"; -import { ComponentChildren } from "preact"; - -export function Fader(props: { children?: ComponentChildren }) { - const ref = useRef(null); - const mounted = useRef(false); - - useEffect(() => { - if (!mounted.current) { - mounted.current = true; - return; - } - if (ref.current) { - ref.current.animate([ - { backgroundColor: "white" }, - { backgroundColor: "peachpuff" }, - ], { - fill: "backwards", - easing: "ease-out", - duration: 600, - }); - } - }); - - return
{props.children}
; -} diff --git a/tests/fixture_partials/islands/InvalidSlot.tsx b/tests/fixture_partials/islands/InvalidSlot.tsx deleted file mode 100644 index 15215497fbd..00000000000 --- a/tests/fixture_partials/islands/InvalidSlot.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { Partial } from "$fresh/runtime.ts"; - -export default function InvalidSlot() { - return ( -
- -

it doesn't work

-
-
- ); -} diff --git a/tests/fixture_partials/islands/KeyExplorer.tsx b/tests/fixture_partials/islands/KeyExplorer.tsx deleted file mode 100644 index c589ea009d1..00000000000 --- a/tests/fixture_partials/islands/KeyExplorer.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { useSignal } from "@preact/signals"; - -export default function KeyExplorer() { - const sig = useSignal(0); - - return ( -
-

counter

-

{sig.value}

- -
- ); -} diff --git a/tests/fixture_partials/islands/LazyLink.tsx b/tests/fixture_partials/islands/LazyLink.tsx deleted file mode 100644 index 9e3d234d277..00000000000 --- a/tests/fixture_partials/islands/LazyLink.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { useSignal } from "@preact/signals"; -import { useEffect } from "preact/hooks"; - -export default function LazyLink(props: { links: string[] }) { - const sig = useSignal(false); - - useEffect(() => { - sig.value = true; - }, []); - - return ( -
- {sig.value - ? ( -
    - {props.links.map((link) => { - return ( -
  • - {link} -
  • - ); - })} -
- ) - : null} -
- ); -} diff --git a/tests/fixture_partials/islands/Logger.tsx b/tests/fixture_partials/islands/Logger.tsx deleted file mode 100644 index a0b1c36a4cf..00000000000 --- a/tests/fixture_partials/islands/Logger.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { ComponentChildren } from "preact"; -import { useEffect, useRef } from "preact/hooks"; - -export function Logger(props: { children?: ComponentChildren; name?: string }) { - const mounted = useRef(false); - - useEffect(() => { - mounted.current = true; - const logs = document.querySelector("#logs"); - if (logs) { - logs.textContent += `mount ${props.name}\n`; - } - - return () => { - if (logs) { - logs.textContent += `unmount ${props.name}\n`; - } - }; - }, []); - - if (mounted.current && typeof document !== "undefined") { - const logs = document.querySelector("#logs"); - if (logs) { - logs.textContent += `update ${props.name}\n`; - } - } - - // deno-lint-ignore no-explicit-any - return props.children as any; -} diff --git a/tests/fixture_partials/islands/Other.tsx b/tests/fixture_partials/islands/Other.tsx deleted file mode 100644 index 501078f6170..00000000000 --- a/tests/fixture_partials/islands/Other.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { useSignal } from "@preact/signals"; -import { Logger } from "./Logger.tsx"; - -export default function Other() { - const sig = useSignal(0); - return ( - -
-

{sig.value}

- -
-
- ); -} diff --git a/tests/fixture_partials/islands/PartialTrigger.tsx b/tests/fixture_partials/islands/PartialTrigger.tsx deleted file mode 100644 index 1ab50a25867..00000000000 --- a/tests/fixture_partials/islands/PartialTrigger.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { Signal } from "@preact/signals"; -import { ComponentChildren } from "preact"; - -export default function PartialTrigger( - props: { - class: string; - href: string; - partial?: string; - loading?: Signal; - children?: ComponentChildren; - }, -) { - return ( - - {props.children} - - ); -} diff --git a/tests/fixture_partials/islands/PassThrough.tsx b/tests/fixture_partials/islands/PassThrough.tsx deleted file mode 100644 index fc9012fb304..00000000000 --- a/tests/fixture_partials/islands/PassThrough.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { Fader } from "$fresh/tests/fixture_partials/islands/Fader.tsx"; -import { Logger } from "./Logger.tsx"; -import { ComponentChildren } from "preact"; - -export default function PassThrough(props: { children?: ComponentChildren }) { - return ( - - -
- {props.children} -
-
-
- ); -} diff --git a/tests/fixture_partials/islands/PropIsland.tsx b/tests/fixture_partials/islands/PropIsland.tsx deleted file mode 100644 index 89cb89023a9..00000000000 --- a/tests/fixture_partials/islands/PropIsland.tsx +++ /dev/null @@ -1,22 +0,0 @@ -export default function PropIsland( - props: { - number: number; - string: string; - boolean: boolean; - strArr: string[]; - obj: { foo: number }; - }, -) { - return ( -
-      {JSON.stringify({
-        number: props.number,
-        string: props.string,
-        null: null,
-        boolean: props.boolean,
-        object: props.obj,
-        strArr: props.strArr,
-      },null,2)}
-    
- ); -} diff --git a/tests/fixture_partials/islands/SignalProp.tsx b/tests/fixture_partials/islands/SignalProp.tsx deleted file mode 100644 index 263593db194..00000000000 --- a/tests/fixture_partials/islands/SignalProp.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { Signal } from "@preact/signals"; - -export default function SignalProp(props: { sig: Signal }) { - return ( -
-

{props.sig.value}

- -
- ); -} diff --git a/tests/fixture_partials/islands/Spinner.tsx b/tests/fixture_partials/islands/Spinner.tsx deleted file mode 100644 index d2c08981377..00000000000 --- a/tests/fixture_partials/islands/Spinner.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import { Signal } from "@preact/signals"; - -export default function Spinner(props: { id: string; show: Signal }) { - return props.show.value - ?

loading...

- : null; -} diff --git a/tests/fixture_partials/islands/Stateful.tsx b/tests/fixture_partials/islands/Stateful.tsx deleted file mode 100644 index d2579baf2a0..00000000000 --- a/tests/fixture_partials/islands/Stateful.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { useState } from "preact/hooks"; - -export default function Stateful(props: { id: string }) { - const [v, set] = useState(0); - return ( -
-

{v}

- -
- ); -} diff --git a/tests/fixture_partials/main.ts b/tests/fixture_partials/main.ts deleted file mode 100644 index 552ac27df71..00000000000 --- a/tests/fixture_partials/main.ts +++ /dev/null @@ -1,9 +0,0 @@ -/// -/// -/// -/// - -import { start } from "$fresh/server.ts"; -import manifest from "./fresh.gen.ts"; - -await start(manifest); diff --git a/tests/fixture_partials/routes/_404.tsx b/tests/fixture_partials/routes/_404.tsx deleted file mode 100644 index c3857ef973b..00000000000 --- a/tests/fixture_partials/routes/_404.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { Partial } from "$fresh/runtime.ts"; - -export default function Page() { - return ( - -

Not found - Error 404

-
- ); -} diff --git a/tests/fixture_partials/routes/_app.tsx b/tests/fixture_partials/routes/_app.tsx deleted file mode 100644 index 1e55e7d35bd..00000000000 --- a/tests/fixture_partials/routes/_app.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { defineApp } from "$fresh/src/server/defines.ts"; - -export default defineApp((res, ctx) => { - return ( - - - - test - - - - - - - ); -}); diff --git a/tests/fixture_partials/routes/active_nav/foo/bar.tsx b/tests/fixture_partials/routes/active_nav/foo/bar.tsx deleted file mode 100644 index 3c0e69be626..00000000000 --- a/tests/fixture_partials/routes/active_nav/foo/bar.tsx +++ /dev/null @@ -1,19 +0,0 @@ -export default function Page() { - return ( -
-

nav

-

- /active_nav/foo/bar -

-

- /active_nav/foo -

-

- /active_nav -

-

- / -

-
- ); -} diff --git a/tests/fixture_partials/routes/active_nav/foo/index.tsx b/tests/fixture_partials/routes/active_nav/foo/index.tsx deleted file mode 100644 index 3c0e69be626..00000000000 --- a/tests/fixture_partials/routes/active_nav/foo/index.tsx +++ /dev/null @@ -1,19 +0,0 @@ -export default function Page() { - return ( -
-

nav

-

- /active_nav/foo/bar -

-

- /active_nav/foo -

-

- /active_nav -

-

- / -

-
- ); -} diff --git a/tests/fixture_partials/routes/active_nav/index.tsx b/tests/fixture_partials/routes/active_nav/index.tsx deleted file mode 100644 index 3c0e69be626..00000000000 --- a/tests/fixture_partials/routes/active_nav/index.tsx +++ /dev/null @@ -1,19 +0,0 @@ -export default function Page() { - return ( -
-

nav

-

- /active_nav/foo/bar -

-

- /active_nav/foo -

-

- /active_nav -

-

- / -

-
- ); -} diff --git a/tests/fixture_partials/routes/active_nav/island.tsx b/tests/fixture_partials/routes/active_nav/island.tsx deleted file mode 100644 index cba29742484..00000000000 --- a/tests/fixture_partials/routes/active_nav/island.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import LazyLink from "../../islands/LazyLink.tsx"; - -export default function Page() { - return ( -
-

active nav island

- -
- ); -} diff --git a/tests/fixture_partials/routes/active_nav_partial/_layout.tsx b/tests/fixture_partials/routes/active_nav_partial/_layout.tsx deleted file mode 100644 index 91321364a38..00000000000 --- a/tests/fixture_partials/routes/active_nav_partial/_layout.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { PageProps } from "$fresh/server.ts"; - -export default function Layout({ Component }: PageProps) { - return ( -
- -
- ); -} diff --git a/tests/fixture_partials/routes/active_nav_partial/foo/bar.tsx b/tests/fixture_partials/routes/active_nav_partial/foo/bar.tsx deleted file mode 100644 index 91a39463abc..00000000000 --- a/tests/fixture_partials/routes/active_nav_partial/foo/bar.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { Partial } from "$fresh/runtime.ts"; - -export default function Page() { - return ( -
- -

/active_nav_partial/foo/bar

-
-

- /active_nav_partial/foo/bar -

-

- /active_nav_partial/foo -

-

- /active_nav_partial -

-

- / -

-
- ); -} diff --git a/tests/fixture_partials/routes/active_nav_partial/foo/index.tsx b/tests/fixture_partials/routes/active_nav_partial/foo/index.tsx deleted file mode 100644 index b0f9ab7ede9..00000000000 --- a/tests/fixture_partials/routes/active_nav_partial/foo/index.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { Partial } from "$fresh/runtime.ts"; - -export default function Page() { - return ( -
- -

/active_nav_partial/foo

-
-

- /active_nav_partial/foo/bar -

-

- /active_nav_partial/foo -

-

- /active_nav_partial -

-

- / -

-
- ); -} diff --git a/tests/fixture_partials/routes/active_nav_partial/index.tsx b/tests/fixture_partials/routes/active_nav_partial/index.tsx deleted file mode 100644 index 467fa73db61..00000000000 --- a/tests/fixture_partials/routes/active_nav_partial/index.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { Partial } from "$fresh/runtime.ts"; - -export default function Page() { - return ( -
- -

/active_nav_partial

-
-

- /active_nav_partial/foo/bar -

-

- /active_nav_partial/foo -

-

- /active_nav_partial -

-

- / -

-
- ); -} diff --git a/tests/fixture_partials/routes/active_nav_partial/island.tsx b/tests/fixture_partials/routes/active_nav_partial/island.tsx deleted file mode 100644 index 52506c92ebe..00000000000 --- a/tests/fixture_partials/routes/active_nav_partial/island.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import LazyLink from "../../islands/LazyLink.tsx"; - -export default function Page() { - return ( -
-

active nav island

- -
- ); -} diff --git a/tests/fixture_partials/routes/button/index.tsx b/tests/fixture_partials/routes/button/index.tsx deleted file mode 100644 index 667bcc1be2f..00000000000 --- a/tests/fixture_partials/routes/button/index.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { Partial } from "$fresh/runtime.ts"; -import { Fader } from "../../islands/Fader.tsx"; - -export default function ModeDemo() { - return ( -
- - -

Initial content

-
-
-

- -

-
- ); -} diff --git a/tests/fixture_partials/routes/button/update.tsx b/tests/fixture_partials/routes/button/update.tsx deleted file mode 100644 index 34540680f92..00000000000 --- a/tests/fixture_partials/routes/button/update.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { defineRoute } from "$fresh/src/server/defines.ts"; -import { RouteConfig } from "$fresh/server.ts"; -import { Partial } from "$fresh/runtime.ts"; -import { Fader } from "../../islands/Fader.tsx"; - -export const config: RouteConfig = { - skipAppWrapper: true, - skipInheritedLayouts: true, -}; - -export default defineRoute((req, ctx) => { - return ( - - -

update

-
-
- ); -}); diff --git a/tests/fixture_partials/routes/client_nav/_layout.tsx b/tests/fixture_partials/routes/client_nav/_layout.tsx deleted file mode 100644 index bce976ac8d8..00000000000 --- a/tests/fixture_partials/routes/client_nav/_layout.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { PageProps } from "$fresh/server.ts"; -import { Partial } from "$fresh/runtime.ts"; -import { Fader } from "../../islands/Fader.tsx"; - -export default function AppLayout({ Component }: PageProps) { - return ( - - - - - fresh title - - - - - - - -

- - Page A - -

-

- - Page B - -

-

- - Page C - -

- -
-      
-    
-  );
-}
diff --git a/tests/fixture_partials/routes/client_nav/index.tsx b/tests/fixture_partials/routes/client_nav/index.tsx
deleted file mode 100644
index bf717a6a55d..00000000000
--- a/tests/fixture_partials/routes/client_nav/index.tsx
+++ /dev/null
@@ -1,21 +0,0 @@
-import { Partial } from "$fresh/runtime.ts";
-import { RouteConfig } from "$fresh/server.ts";
-import CounterA from "../../islands/CounterA.tsx";
-import { Fader } from "../../islands/Fader.tsx";
-
-export const config: RouteConfig = {
-  skipAppWrapper: true,
-};
-
-export default function ModeDemo() {
-  return (
-    
- - -

Initial content

- -
-
-
- ); -} diff --git a/tests/fixture_partials/routes/client_nav/injected.tsx b/tests/fixture_partials/routes/client_nav/injected.tsx deleted file mode 100644 index 3b9b5a2901f..00000000000 --- a/tests/fixture_partials/routes/client_nav/injected.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { defineRoute } from "$fresh/src/server/defines.ts"; - -export default defineRoute(() => { - return new Response("", { - status: 302, - headers: { - Location: "/client_nav", - }, - }); -}); diff --git a/tests/fixture_partials/routes/client_nav/page-a.tsx b/tests/fixture_partials/routes/client_nav/page-a.tsx deleted file mode 100644 index 94fcfa0bb51..00000000000 --- a/tests/fixture_partials/routes/client_nav/page-a.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { RouteConfig } from "$fresh/server.ts"; -import CounterA from "../../islands/CounterA.tsx"; -import { Fader } from "../../islands/Fader.tsx"; - -export const config: RouteConfig = { - skipAppWrapper: true, -}; - -export default function PageA() { - return ( - -

Page A

-

asdfasdf asdf asdf

- -
- ); -} diff --git a/tests/fixture_partials/routes/client_nav/page-b.tsx b/tests/fixture_partials/routes/client_nav/page-b.tsx deleted file mode 100644 index 70468dc22d3..00000000000 --- a/tests/fixture_partials/routes/client_nav/page-b.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { RouteConfig } from "$fresh/server.ts"; -import { Fader } from "../../islands/Fader.tsx"; -import CounterB from "../../islands/CounterB.tsx"; - -export const config: RouteConfig = { - skipAppWrapper: true, -}; - -export default function PageB() { - return ( - - something before -
-

Page B

-
- -

asdfasdf asdf asdf

-
- ); -} diff --git a/tests/fixture_partials/routes/client_nav/page-c.tsx b/tests/fixture_partials/routes/client_nav/page-c.tsx deleted file mode 100644 index 577849e70a6..00000000000 --- a/tests/fixture_partials/routes/client_nav/page-c.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { RouteConfig } from "$fresh/server.ts"; -import { Fader } from "../../islands/Fader.tsx"; - -export const config: RouteConfig = { - skipAppWrapper: true, -}; - -export default function PageB() { - return ( - -

Page C

- -

asdfasdf asdf asdf

-
-
- ); -} diff --git a/tests/fixture_partials/routes/client_nav_both/_layout.tsx b/tests/fixture_partials/routes/client_nav_both/_layout.tsx deleted file mode 100644 index 4310f99fa8d..00000000000 --- a/tests/fixture_partials/routes/client_nav_both/_layout.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import { PageProps } from "$fresh/server.ts"; -import { Partial } from "$fresh/runtime.ts"; -import { Fader } from "../../islands/Fader.tsx"; - -export default function AppLayout({ Component }: PageProps) { - return ( - - - - - fresh title - - - - - - - - - -
-      
-    
-  );
-}
diff --git a/tests/fixture_partials/routes/client_nav_both/index.tsx b/tests/fixture_partials/routes/client_nav_both/index.tsx
deleted file mode 100644
index bf717a6a55d..00000000000
--- a/tests/fixture_partials/routes/client_nav_both/index.tsx
+++ /dev/null
@@ -1,21 +0,0 @@
-import { Partial } from "$fresh/runtime.ts";
-import { RouteConfig } from "$fresh/server.ts";
-import CounterA from "../../islands/CounterA.tsx";
-import { Fader } from "../../islands/Fader.tsx";
-
-export const config: RouteConfig = {
-  skipAppWrapper: true,
-};
-
-export default function ModeDemo() {
-  return (
-    
- - -

Initial content

- -
-
-
- ); -} diff --git a/tests/fixture_partials/routes/client_nav_both/page-a.tsx b/tests/fixture_partials/routes/client_nav_both/page-a.tsx deleted file mode 100644 index 94fcfa0bb51..00000000000 --- a/tests/fixture_partials/routes/client_nav_both/page-a.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { RouteConfig } from "$fresh/server.ts"; -import CounterA from "../../islands/CounterA.tsx"; -import { Fader } from "../../islands/Fader.tsx"; - -export const config: RouteConfig = { - skipAppWrapper: true, -}; - -export default function PageA() { - return ( - -

Page A

-

asdfasdf asdf asdf

- -
- ); -} diff --git a/tests/fixture_partials/routes/client_nav_both/page-b.tsx b/tests/fixture_partials/routes/client_nav_both/page-b.tsx deleted file mode 100644 index 70468dc22d3..00000000000 --- a/tests/fixture_partials/routes/client_nav_both/page-b.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { RouteConfig } from "$fresh/server.ts"; -import { Fader } from "../../islands/Fader.tsx"; -import CounterB from "../../islands/CounterB.tsx"; - -export const config: RouteConfig = { - skipAppWrapper: true, -}; - -export default function PageB() { - return ( - - something before -
-

Page B

-
- -

asdfasdf asdf asdf

-
- ); -} diff --git a/tests/fixture_partials/routes/client_nav_both/page-c.tsx b/tests/fixture_partials/routes/client_nav_both/page-c.tsx deleted file mode 100644 index 577849e70a6..00000000000 --- a/tests/fixture_partials/routes/client_nav_both/page-c.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { RouteConfig } from "$fresh/server.ts"; -import { Fader } from "../../islands/Fader.tsx"; - -export const config: RouteConfig = { - skipAppWrapper: true, -}; - -export default function PageB() { - return ( - -

Page C

- -

asdfasdf asdf asdf

-
-
- ); -} diff --git a/tests/fixture_partials/routes/client_nav_opt_out/_layout.tsx b/tests/fixture_partials/routes/client_nav_opt_out/_layout.tsx deleted file mode 100644 index d4194a2d9db..00000000000 --- a/tests/fixture_partials/routes/client_nav_opt_out/_layout.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { PageProps } from "$fresh/server.ts"; -import { Partial } from "$fresh/runtime.ts"; -import { Fader } from "../../islands/Fader.tsx"; - -export default function AppLayout({ Component }: PageProps) { - return ( - - - - - fresh title - - - - - - - -

- - Page A - -

-

- - Page B - -

-

- - Page C - -

- -
-      
-    
-  );
-}
diff --git a/tests/fixture_partials/routes/client_nav_opt_out/index.tsx b/tests/fixture_partials/routes/client_nav_opt_out/index.tsx
deleted file mode 100644
index bf717a6a55d..00000000000
--- a/tests/fixture_partials/routes/client_nav_opt_out/index.tsx
+++ /dev/null
@@ -1,21 +0,0 @@
-import { Partial } from "$fresh/runtime.ts";
-import { RouteConfig } from "$fresh/server.ts";
-import CounterA from "../../islands/CounterA.tsx";
-import { Fader } from "../../islands/Fader.tsx";
-
-export const config: RouteConfig = {
-  skipAppWrapper: true,
-};
-
-export default function ModeDemo() {
-  return (
-    
- - -

Initial content

- -
-
-
- ); -} diff --git a/tests/fixture_partials/routes/client_nav_opt_out/injected.tsx b/tests/fixture_partials/routes/client_nav_opt_out/injected.tsx deleted file mode 100644 index 52fc8fffb8b..00000000000 --- a/tests/fixture_partials/routes/client_nav_opt_out/injected.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { defineRoute } from "$fresh/src/server/defines.ts"; - -export default defineRoute(() => { - return new Response("", { - status: 302, - headers: { - Location: "/client_nav_opt_out", - }, - }); -}); diff --git a/tests/fixture_partials/routes/client_nav_opt_out/page-a.tsx b/tests/fixture_partials/routes/client_nav_opt_out/page-a.tsx deleted file mode 100644 index 94fcfa0bb51..00000000000 --- a/tests/fixture_partials/routes/client_nav_opt_out/page-a.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { RouteConfig } from "$fresh/server.ts"; -import CounterA from "../../islands/CounterA.tsx"; -import { Fader } from "../../islands/Fader.tsx"; - -export const config: RouteConfig = { - skipAppWrapper: true, -}; - -export default function PageA() { - return ( - -

Page A

-

asdfasdf asdf asdf

- -
- ); -} diff --git a/tests/fixture_partials/routes/client_nav_opt_out/page-b.tsx b/tests/fixture_partials/routes/client_nav_opt_out/page-b.tsx deleted file mode 100644 index 70468dc22d3..00000000000 --- a/tests/fixture_partials/routes/client_nav_opt_out/page-b.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { RouteConfig } from "$fresh/server.ts"; -import { Fader } from "../../islands/Fader.tsx"; -import CounterB from "../../islands/CounterB.tsx"; - -export const config: RouteConfig = { - skipAppWrapper: true, -}; - -export default function PageB() { - return ( - - something before -
-

Page B

-
- -

asdfasdf asdf asdf

-
- ); -} diff --git a/tests/fixture_partials/routes/client_nav_opt_out/page-c.tsx b/tests/fixture_partials/routes/client_nav_opt_out/page-c.tsx deleted file mode 100644 index 577849e70a6..00000000000 --- a/tests/fixture_partials/routes/client_nav_opt_out/page-c.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { RouteConfig } from "$fresh/server.ts"; -import { Fader } from "../../islands/Fader.tsx"; - -export const config: RouteConfig = { - skipAppWrapper: true, -}; - -export default function PageB() { - return ( - -

Page C

- -

asdfasdf asdf asdf

-
-
- ); -} diff --git a/tests/fixture_partials/routes/deep_partial/index.tsx b/tests/fixture_partials/routes/deep_partial/index.tsx deleted file mode 100644 index 756672062b2..00000000000 --- a/tests/fixture_partials/routes/deep_partial/index.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { Partial } from "$fresh/runtime.ts"; -import { Fader } from "../../islands/Fader.tsx"; -import { Logger } from "../../islands/Logger.tsx"; - -export default function SlotDemo() { - return ( -
- - - -

initial

-
-
-
-

- - update - -

-
-    
- ); -} diff --git a/tests/fixture_partials/routes/deep_partial/injected.tsx b/tests/fixture_partials/routes/deep_partial/injected.tsx deleted file mode 100644 index f1fd4145f54..00000000000 --- a/tests/fixture_partials/routes/deep_partial/injected.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { defineRoute } from "$fresh/src/server/defines.ts"; - -export default defineRoute(() => { - return new Response("", { - status: 302, - headers: { - Location: "/deep_partial", - }, - }); -}); diff --git a/tests/fixture_partials/routes/deep_partial/update.tsx b/tests/fixture_partials/routes/deep_partial/update.tsx deleted file mode 100644 index 2dd2665c9e1..00000000000 --- a/tests/fixture_partials/routes/deep_partial/update.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { defineRoute } from "$fresh/src/server/defines.ts"; -import { RouteConfig } from "$fresh/server.ts"; -import { Partial } from "$fresh/runtime.ts"; -import { Fader } from "../../islands/Fader.tsx"; -import { Logger } from "../../islands/Logger.tsx"; - -export const config: RouteConfig = { - skipAppWrapper: true, - skipInheritedLayouts: true, -}; - -export default defineRoute((req, ctx) => { - return ( -
-
- - - -

updated

-
-
-
-
-
- ); -}); diff --git a/tests/fixture_partials/routes/duplicate_name/index.tsx b/tests/fixture_partials/routes/duplicate_name/index.tsx deleted file mode 100644 index cb9ad298efe..00000000000 --- a/tests/fixture_partials/routes/duplicate_name/index.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { Partial } from "$fresh/runtime.ts"; -import { PageProps } from "$fresh/server.ts"; - -export default function SlotDemo(props: PageProps) { - const update = props.url.searchParams.has("swap"); - - return ( -
- -

foo

-

some text

-
- {update && ( - -

foo

-
- )} -

- - swap - -

-
- ); -} diff --git a/tests/fixture_partials/routes/error_404.tsx b/tests/fixture_partials/routes/error_404.tsx deleted file mode 100644 index b58683db60f..00000000000 --- a/tests/fixture_partials/routes/error_404.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { Partial } from "$fresh/runtime.ts"; - -export default function ModeDemo() { - return ( -
- -

default content

-
-

- - update - -

-
- ); -} diff --git a/tests/fixture_partials/routes/form/index.tsx b/tests/fixture_partials/routes/form/index.tsx deleted file mode 100644 index 7e911445fb3..00000000000 --- a/tests/fixture_partials/routes/form/index.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { Partial } from "$fresh/runtime.ts"; - -export default function SlotDemo() { - return ( -
- - -

Default content

-

- -

-
- - -
- ); -} diff --git a/tests/fixture_partials/routes/form/injected.tsx b/tests/fixture_partials/routes/form/injected.tsx deleted file mode 100644 index c26cf8ac72e..00000000000 --- a/tests/fixture_partials/routes/form/injected.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { defineRoute } from "$fresh/src/server/defines.ts"; - -export default defineRoute(() => { - return new Response("", { - status: 302, - headers: { - Location: "/form", - }, - }); -}); diff --git a/tests/fixture_partials/routes/form/update.tsx b/tests/fixture_partials/routes/form/update.tsx deleted file mode 100644 index 61b9d82204a..00000000000 --- a/tests/fixture_partials/routes/form/update.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { defineRoute } from "$fresh/src/server/defines.ts"; -import { RouteConfig } from "$fresh/server.ts"; -import { Partial } from "$fresh/runtime.ts"; - -export const config: RouteConfig = { - skipAppWrapper: true, - skipInheritedLayouts: true, -}; - -export default defineRoute((req, ctx) => { - return ( - -

it works

-

- -

-
- ); -}); diff --git a/tests/fixture_partials/routes/form_get/index.tsx b/tests/fixture_partials/routes/form_get/index.tsx deleted file mode 100644 index 43036036df2..00000000000 --- a/tests/fixture_partials/routes/form_get/index.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { Partial } from "$fresh/runtime.ts"; -import { PageProps } from "$fresh/server.ts"; -import { Logger } from "../../islands/Logger.tsx"; - -export default function SlotDemo(props: PageProps) { - let value = props.url.searchParams.get("name") ?? ""; - - value += value ? "_foo" : ""; - - return ( -
-
- -

Default content

-

- -

- -

{props.url.toString()}

-
- -
-
-    
- ); -} diff --git a/tests/fixture_partials/routes/form_post/index.tsx b/tests/fixture_partials/routes/form_post/index.tsx deleted file mode 100644 index 621db8caa89..00000000000 --- a/tests/fixture_partials/routes/form_post/index.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { Partial } from "$fresh/runtime.ts"; -import { defineRoute, Handlers } from "$fresh/server.ts"; -import { Logger } from "../../islands/Logger.tsx"; - -export const handler: Handlers = { - POST(req, ctx) { - return ctx.render(); - }, -}; - -export default defineRoute(async (req, ctx) => { - let value = ""; - - if (req.body !== null) { - const data = await req.formData(); - value += data.has("name") ? data.get("name") + "_foo" : ""; - } - - return ( -
-
- -

Default content

-

- -

- -

{ctx.url.toString()}

-
- -
-
-    
- ); -}); diff --git a/tests/fixture_partials/routes/form_submitter/index.tsx b/tests/fixture_partials/routes/form_submitter/index.tsx deleted file mode 100644 index 84b965b8597..00000000000 --- a/tests/fixture_partials/routes/form_submitter/index.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { Partial } from "$fresh/runtime.ts"; -import { defineRoute, Handlers } from "$fresh/server.ts"; -import { Logger } from "../../islands/Logger.tsx"; - -export const handler: Handlers = { - POST(req, ctx) { - return ctx.render(); - }, -}; - -export default defineRoute(async (req, ctx) => { - let value = ""; - - if (req.body !== null) { - const data = await req.formData(); - value += data.has("name") ? data.get("name") + "_foo" : ""; - } - - return ( -
-
- -

Default content

-

- -

- -

{ctx.url.toString()}

-
-
- -
-    
- ); -}); diff --git a/tests/fixture_partials/routes/form_submitter_partial/index.tsx b/tests/fixture_partials/routes/form_submitter_partial/index.tsx deleted file mode 100644 index a157a8da57b..00000000000 --- a/tests/fixture_partials/routes/form_submitter_partial/index.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import { Partial } from "$fresh/runtime.ts"; -import { defineRoute, Handlers } from "$fresh/server.ts"; -import { Logger } from "../../islands/Logger.tsx"; - -export const handler: Handlers = { - POST(req, ctx) { - return ctx.render(); - }, -}; - -export default defineRoute(async (req, ctx) => { - let value = ""; - - if (req.body !== null) { - const data = await req.formData(); - value += data.has("name") ? data.get("name") + "_foo" : ""; - } - - return ( -
-
- -

Default content

-

- -

- -

{ctx.url.toString()}

-
-
- -
-    
- ); -}); diff --git a/tests/fixture_partials/routes/form_submitter_partial_no_client_nav/index.tsx b/tests/fixture_partials/routes/form_submitter_partial_no_client_nav/index.tsx deleted file mode 100644 index 83f85795537..00000000000 --- a/tests/fixture_partials/routes/form_submitter_partial_no_client_nav/index.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import { Partial } from "$fresh/runtime.ts"; -import { defineRoute, Handlers } from "$fresh/server.ts"; -import { Logger } from "../../islands/Logger.tsx"; - -export const handler: Handlers = { - POST(req, ctx) { - return ctx.render(); - }, -}; - -export default defineRoute(async (req, ctx) => { - let value = ""; - - if (req.body !== null) { - const data = await req.formData(); - value += data.has("name") ? data.get("name") + "_foo" : ""; - } - - return ( -
-
- -

Default content

-

- -

- -

{ctx.url.toString()}

-
-
-
- -
-
-    
- ); -}); diff --git a/tests/fixture_partials/routes/fragment_nav.tsx b/tests/fixture_partials/routes/fragment_nav.tsx deleted file mode 100644 index 88eecfe40a6..00000000000 --- a/tests/fixture_partials/routes/fragment_nav.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { Partial } from "$fresh/runtime.ts"; - -export default function SlotDemo() { - return ( -
-

Same nav

- #foo - -

- foo partial -

-
-
- ); -} diff --git a/tests/fixture_partials/routes/fragment_nav_scroll.tsx b/tests/fixture_partials/routes/fragment_nav_scroll.tsx deleted file mode 100644 index 02c1ada52e4..00000000000 --- a/tests/fixture_partials/routes/fragment_nav_scroll.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import { Partial } from "$fresh/runtime.ts"; - -export default function SlotDemo() { - return ( -
-

asdf

-

asdf

-

asdf

-

asdf

-

asdf

-

asdf

-

asdf

-

asdf

-

asdf

-

asdf

-

asdf

-

asdf

-

asdf

-

asdf

-

asdf

-

asdf

-

asdf

-

asdf

-

asdf

-

asdf

-

asdf

-

asdf

-

asdf

-

asdf

-

asdf

-

asdf

-

asdf

-

asdf

-

asdf

-

asdf

-

asdf

-

asdf

-

asdf

-

asdf

-

asdf

-

asdf

-

asdf

-

asdf

-

asdf

-

asdf

-

asdf

-

asdf

-

asdf

-

asdf

-

asdf

-

asdf

-

asdf

-

asdf

-

asdf

-

asdf

-

asdf

-

asdf

-

asdf

-

asdf

-

asdf

-

asdf

-

asdf

-

asdf

-

asdf

-

asdf

-

asdf

-

asdf

-

Same nav

- #foo - -

- foo partial -

-
-
- ); -} diff --git a/tests/fixture_partials/routes/head_merge/duplicate.tsx b/tests/fixture_partials/routes/head_merge/duplicate.tsx deleted file mode 100644 index 6cec460957c..00000000000 --- a/tests/fixture_partials/routes/head_merge/duplicate.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { defineRoute } from "$fresh/src/server/defines.ts"; -import { RouteConfig } from "$fresh/server.ts"; -import { Head, Partial } from "$fresh/runtime.ts"; -import { Fader } from "../../islands/Fader.tsx"; - -export const config: RouteConfig = { - skipAppWrapper: true, - skipInheritedLayouts: true, -}; - -export default defineRoute((req, ctx) => { - return ( - <> - - Head merge duplicated - - - - - -

duplicated

-

duplicated content

-
-
- - ); -}); diff --git a/tests/fixture_partials/routes/head_merge/index.tsx b/tests/fixture_partials/routes/head_merge/index.tsx deleted file mode 100644 index cc59d3b8906..00000000000 --- a/tests/fixture_partials/routes/head_merge/index.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import { Head, Partial } from "$fresh/runtime.ts"; -import { Fader } from "../../islands/Fader.tsx"; - -export default function SlotDemo() { - return ( -
- - Head merge - - - - - - -

Initial content

-
-
-

- - update - -

-

- - duplicate - -

-

- - without title - -

-
- ); -} diff --git a/tests/fixture_partials/routes/head_merge/injected.tsx b/tests/fixture_partials/routes/head_merge/injected.tsx deleted file mode 100644 index a3fdc9c3d15..00000000000 --- a/tests/fixture_partials/routes/head_merge/injected.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { defineRoute } from "$fresh/src/server/defines.ts"; - -export default defineRoute(() => { - return new Response("", { - status: 302, - headers: { - Location: "/head_merge", - }, - }); -}); diff --git a/tests/fixture_partials/routes/head_merge/update.tsx b/tests/fixture_partials/routes/head_merge/update.tsx deleted file mode 100644 index fe7cf772992..00000000000 --- a/tests/fixture_partials/routes/head_merge/update.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { defineRoute } from "$fresh/src/server/defines.ts"; -import { RouteConfig } from "$fresh/server.ts"; -import { Head, Partial } from "$fresh/runtime.ts"; -import { Fader } from "../../islands/Fader.tsx"; - -export const config: RouteConfig = { - skipAppWrapper: true, - skipInheritedLayouts: true, -}; - -export default defineRoute((req, ctx) => { - return ( - <> - - Head merge updated - - - - - - - - -

updated

-

updated content

-
-
- - ); -}); diff --git a/tests/fixture_partials/routes/head_merge/without_title.tsx b/tests/fixture_partials/routes/head_merge/without_title.tsx deleted file mode 100644 index 15d0d559931..00000000000 --- a/tests/fixture_partials/routes/head_merge/without_title.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { defineRoute } from "$fresh/src/server/defines.ts"; -import { RouteConfig } from "$fresh/server.ts"; -import { Head, Partial } from "$fresh/runtime.ts"; -import { Fader } from "../../islands/Fader.tsx"; - -export const config: RouteConfig = { - skipAppWrapper: true, - skipInheritedLayouts: true, -}; - -export default defineRoute((req, ctx) => { - return ( - <> - - - - - -

page without title

-
-
- - ); -}); diff --git a/tests/fixture_partials/routes/index.tsx b/tests/fixture_partials/routes/index.tsx deleted file mode 100644 index d50c5fee82e..00000000000 --- a/tests/fixture_partials/routes/index.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { basename, dirname, extname, fromFileUrl } from "../../deps.ts"; - -const __dirname = dirname(fromFileUrl(import.meta.url)); - -const links: string[] = []; -for (const file of Deno.readDirSync(__dirname)) { - if (file.name.startsWith("index")) continue; - const name = basename(file.name, extname(file.name)); - links.push(name); -} - -export default function Home() { - return ( -
-

Tests

-
    - {links.sort().map((link) => { - return ( -
  • - {link} -
  • - ); - })} -
-
- ); -} diff --git a/tests/fixture_partials/routes/isPartial/_middleware.ts b/tests/fixture_partials/routes/isPartial/_middleware.ts deleted file mode 100644 index 24f04308c4a..00000000000 --- a/tests/fixture_partials/routes/isPartial/_middleware.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { FreshContext } from "$fresh/server.ts"; - -export type IsPartialInContextState = { - setFromMiddleware: boolean; - notSetFromMiddleware: boolean; -}; - -export async function handler( - _req: Request, - ctx: FreshContext, -) { - if (ctx.isPartial) { - ctx.state.setFromMiddleware = true; - } else { - ctx.state.notSetFromMiddleware = true; - } - return await ctx.next(); -} diff --git a/tests/fixture_partials/routes/isPartial/async.tsx b/tests/fixture_partials/routes/isPartial/async.tsx deleted file mode 100644 index 4285c284c31..00000000000 --- a/tests/fixture_partials/routes/isPartial/async.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { defineRoute, RouteConfig } from "$fresh/server.ts"; -import { IsPartialInContextState } from "./_middleware.ts"; -import { Partial } from "$fresh/runtime.ts"; - -export const config: RouteConfig = { - skipAppWrapper: true, - skipInheritedLayouts: true, -}; - -export default defineRoute((req, ctx) => { - const result = { - isPartial: ctx.isPartial, - setFromMiddleware: ctx.state.setFromMiddleware, - notSetFromMiddleware: ctx.state.notSetFromMiddleware, - }; - if (ctx.isPartial) { - return ( - -
{JSON.stringify(result, null, 2)}
-
- ); - } - return
{JSON.stringify(result, null, 2)}
; -}); diff --git a/tests/fixture_partials/routes/isPartial/handler.tsx b/tests/fixture_partials/routes/isPartial/handler.tsx deleted file mode 100644 index 12c33a347d6..00000000000 --- a/tests/fixture_partials/routes/isPartial/handler.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { Handlers, PageProps, RouteConfig } from "$fresh/server.ts"; -import { IsPartialInContextState } from "./_middleware.ts"; -import { Partial } from "$fresh/runtime.ts"; - -export const config: RouteConfig = { - skipAppWrapper: true, - skipInheritedLayouts: true, -}; - -export const handler: Handlers = { - GET(_req, ctx) { - return ctx.render(ctx.isPartial); - }, -}; - -export default function Home( - props: PageProps, -) { - const result = { - isPartial: props.data, - setFromMiddleware: props.state.setFromMiddleware, - notSetFromMiddleware: props.state.notSetFromMiddleware, - }; - if (props.data) { - return ( - -
{JSON.stringify(result, null, 2)}
-
- ); - } - return
{JSON.stringify(result, null, 2)}
; -} diff --git a/tests/fixture_partials/routes/isPartial/index.tsx b/tests/fixture_partials/routes/isPartial/index.tsx deleted file mode 100644 index bb63df962ff..00000000000 --- a/tests/fixture_partials/routes/isPartial/index.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { Partial } from "$fresh/runtime.ts"; - -export default function SlotDemo() { - return ( -
-
- -

Default content

-
-
- - handler update - -
- - async update - -
- ); -} diff --git a/tests/fixture_partials/routes/isPartial/injected.tsx b/tests/fixture_partials/routes/isPartial/injected.tsx deleted file mode 100644 index 4908eeeb9fc..00000000000 --- a/tests/fixture_partials/routes/isPartial/injected.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { defineRoute } from "$fresh/src/server/defines.ts"; - -export default defineRoute(() => { - return new Response("", { - status: 302, - headers: { - Location: "/isPartial", - }, - }); -}); diff --git a/tests/fixture_partials/routes/island_instance/index.tsx b/tests/fixture_partials/routes/island_instance/index.tsx deleted file mode 100644 index 7b40fba012c..00000000000 --- a/tests/fixture_partials/routes/island_instance/index.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import { Partial } from "$fresh/runtime.ts"; -import CounterA from "../../islands/CounterA.tsx"; -import { Fader } from "../../islands/Fader.tsx"; - -export default function SlotDemo() { - return ( -
- - -

Initial content

- -
-
-

- - Update - -

-

- - Remove - -

-

- - Replace - -

-
-    
- ); -} diff --git a/tests/fixture_partials/routes/island_instance/injected.tsx b/tests/fixture_partials/routes/island_instance/injected.tsx deleted file mode 100644 index cb6b2285c5d..00000000000 --- a/tests/fixture_partials/routes/island_instance/injected.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { defineRoute } from "$fresh/src/server/defines.ts"; - -export default defineRoute(() => { - return new Response("", { - status: 302, - headers: { - Location: "/island_instance", - }, - }); -}); diff --git a/tests/fixture_partials/routes/island_instance/partial.tsx b/tests/fixture_partials/routes/island_instance/partial.tsx deleted file mode 100644 index ba517449af6..00000000000 --- a/tests/fixture_partials/routes/island_instance/partial.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { defineRoute } from "$fresh/src/server/defines.ts"; -import { RouteConfig } from "$fresh/server.ts"; -import { Partial } from "$fresh/runtime.ts"; -import CounterA from "../../islands/CounterA.tsx"; -import { Fader } from "../../islands/Fader.tsx"; - -export const config: RouteConfig = { - skipAppWrapper: true, - skipInheritedLayouts: true, -}; - -export default defineRoute((req, ctx) => { - return ( - - -

Another page

-

updated content

- -
-
- ); -}); diff --git a/tests/fixture_partials/routes/island_instance/partial_remove.tsx b/tests/fixture_partials/routes/island_instance/partial_remove.tsx deleted file mode 100644 index 1490c7c7da9..00000000000 --- a/tests/fixture_partials/routes/island_instance/partial_remove.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { defineRoute } from "$fresh/src/server/defines.ts"; -import { RouteConfig } from "$fresh/server.ts"; -import { Partial } from "$fresh/runtime.ts"; -import { Fader } from "../../islands/Fader.tsx"; - -export const config: RouteConfig = { - skipAppWrapper: true, - skipInheritedLayouts: true, -}; - -export default defineRoute((req, ctx) => { - return ( - - -

no islands

-
-
- ); -}); diff --git a/tests/fixture_partials/routes/island_instance/partial_replace.tsx b/tests/fixture_partials/routes/island_instance/partial_replace.tsx deleted file mode 100644 index d1d83b36315..00000000000 --- a/tests/fixture_partials/routes/island_instance/partial_replace.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { defineRoute } from "$fresh/src/server/defines.ts"; -import { RouteConfig } from "$fresh/server.ts"; -import { Partial } from "$fresh/runtime.ts"; -import { Fader } from "../../islands/Fader.tsx"; -import Other from "../../islands/Other.tsx"; - -export const config: RouteConfig = { - skipAppWrapper: true, - skipInheritedLayouts: true, -}; - -export default defineRoute((req, ctx) => { - return ( - - -

replaced content

- -
-
- ); -}); diff --git a/tests/fixture_partials/routes/island_instance_multiple/index.tsx b/tests/fixture_partials/routes/island_instance_multiple/index.tsx deleted file mode 100644 index 38315fe96a9..00000000000 --- a/tests/fixture_partials/routes/island_instance_multiple/index.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { Partial } from "$fresh/runtime.ts"; -import CounterA from "../../islands/CounterA.tsx"; -import CounterB from "../../islands/CounterB.tsx"; -import { Fader } from "../../islands/Fader.tsx"; - -export default function SlotDemo() { - return ( -
-
- - -

Initial content slot 1

- -
-
-
- - -

Initial content slot 2

- -
-
-
-
-

- - update second - -

-

- - update both - -

-
-    
- ); -} diff --git a/tests/fixture_partials/routes/island_instance_multiple/injected.tsx b/tests/fixture_partials/routes/island_instance_multiple/injected.tsx deleted file mode 100644 index 7c596212e97..00000000000 --- a/tests/fixture_partials/routes/island_instance_multiple/injected.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { defineRoute } from "$fresh/src/server/defines.ts"; - -export default defineRoute(() => { - return new Response("", { - status: 302, - headers: { - Location: "/island_instance_multiple", - }, - }); -}); diff --git a/tests/fixture_partials/routes/island_instance_multiple/partial.tsx b/tests/fixture_partials/routes/island_instance_multiple/partial.tsx deleted file mode 100644 index 36fbcebbe08..00000000000 --- a/tests/fixture_partials/routes/island_instance_multiple/partial.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { defineRoute } from "$fresh/src/server/defines.ts"; -import { RouteConfig } from "$fresh/server.ts"; -import { Partial } from "$fresh/runtime.ts"; -import CounterB from "../../islands/CounterB.tsx"; -import { Fader } from "../../islands/Fader.tsx"; - -export const config: RouteConfig = { - skipAppWrapper: true, - skipInheritedLayouts: true, -}; - -export default defineRoute((req, ctx) => { - return ( - - -

Another page

-

updated content {Math.random()}

- -
-
- ); -}); diff --git a/tests/fixture_partials/routes/island_instance_multiple/partial_both.tsx b/tests/fixture_partials/routes/island_instance_multiple/partial_both.tsx deleted file mode 100644 index b2d962c1768..00000000000 --- a/tests/fixture_partials/routes/island_instance_multiple/partial_both.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { defineRoute } from "$fresh/src/server/defines.ts"; -import { RouteConfig } from "$fresh/server.ts"; -import { Partial } from "$fresh/runtime.ts"; -import CounterA from "../../islands/CounterA.tsx"; -import CounterB from "../../islands/CounterB.tsx"; -import { Fader } from "../../islands/Fader.tsx"; - -export const config: RouteConfig = { - skipAppWrapper: true, - skipInheritedLayouts: true, -}; - -export default defineRoute((req, ctx) => { - return ( - <> - - -

updated content {Math.random()}

- -
-
- - -

Another page

-

updated content {Math.random()}

- -
-
- - ); -}); diff --git a/tests/fixture_partials/routes/island_instance_nested/index.tsx b/tests/fixture_partials/routes/island_instance_nested/index.tsx deleted file mode 100644 index 4a3d3c4735a..00000000000 --- a/tests/fixture_partials/routes/island_instance_nested/index.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import { Partial } from "$fresh/runtime.ts"; -import CounterA from "../../islands/CounterA.tsx"; -import CounterB from "../../islands/CounterB.tsx"; -import PassThrough from "../../islands/PassThrough.tsx"; - -export default function SlotDemo() { - return ( -
- - -
-

server content

- -
-
- -

another pass through

- -
-
-
-
-

- - update - -

-

- - replace - -

-
-    
- ); -} diff --git a/tests/fixture_partials/routes/island_instance_nested/injected.tsx b/tests/fixture_partials/routes/island_instance_nested/injected.tsx deleted file mode 100644 index 28047c46643..00000000000 --- a/tests/fixture_partials/routes/island_instance_nested/injected.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { defineRoute } from "$fresh/src/server/defines.ts"; - -export default defineRoute(() => { - return new Response("", { - status: 302, - headers: { - Location: "/island_instance_nested", - }, - }); -}); diff --git a/tests/fixture_partials/routes/island_instance_nested/partial.tsx b/tests/fixture_partials/routes/island_instance_nested/partial.tsx deleted file mode 100644 index 68ef0be9b43..00000000000 --- a/tests/fixture_partials/routes/island_instance_nested/partial.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { defineRoute } from "$fresh/src/server/defines.ts"; -import { RouteConfig } from "$fresh/server.ts"; -import { Partial } from "$fresh/runtime.ts"; -import CounterA from "../../islands/CounterA.tsx"; -import CounterB from "../../islands/CounterB.tsx"; -import PassThrough from "../../islands/PassThrough.tsx"; - -export const config: RouteConfig = { - skipAppWrapper: true, - skipInheritedLayouts: true, -}; - -export default defineRoute((req, ctx) => { - return ( - - -
-

updated server content

- -
-
- -

another pass through

- -
-
-
- ); -}); diff --git a/tests/fixture_partials/routes/island_instance_nested/replace.tsx b/tests/fixture_partials/routes/island_instance_nested/replace.tsx deleted file mode 100644 index cca21480585..00000000000 --- a/tests/fixture_partials/routes/island_instance_nested/replace.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { defineRoute } from "$fresh/src/server/defines.ts"; -import { RouteConfig } from "$fresh/server.ts"; -import { Partial } from "$fresh/runtime.ts"; -import CounterA from "../../islands/CounterA.tsx"; - -export const config: RouteConfig = { - skipAppWrapper: true, - skipInheritedLayouts: true, -}; - -export default defineRoute((req, ctx) => { - return ( - - - - ); -}); diff --git a/tests/fixture_partials/routes/island_props/index.tsx b/tests/fixture_partials/routes/island_props/index.tsx deleted file mode 100644 index 8e124660d99..00000000000 --- a/tests/fixture_partials/routes/island_props/index.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { Partial } from "$fresh/runtime.ts"; -import PropIsland from "../../islands/PropIsland.tsx"; - -export default function PropsDemo() { - return ( -
- -

initial

- -
-

- - Update - -

-
-    
- ); -} diff --git a/tests/fixture_partials/routes/island_props/injected.tsx b/tests/fixture_partials/routes/island_props/injected.tsx deleted file mode 100644 index 354492a8989..00000000000 --- a/tests/fixture_partials/routes/island_props/injected.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { defineRoute } from "$fresh/src/server/defines.ts"; - -export default defineRoute(() => { - return new Response("", { - status: 302, - headers: { - Location: "/island_props", - }, - }); -}); diff --git a/tests/fixture_partials/routes/island_props/partial.tsx b/tests/fixture_partials/routes/island_props/partial.tsx deleted file mode 100644 index 77871ee8ecd..00000000000 --- a/tests/fixture_partials/routes/island_props/partial.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { defineRoute } from "$fresh/src/server/defines.ts"; -import { RouteConfig } from "$fresh/server.ts"; -import { Partial } from "$fresh/runtime.ts"; -import { Fader } from "../../islands/Fader.tsx"; -import PropIsland from "../../islands/PropIsland.tsx"; - -export const config: RouteConfig = { - skipAppWrapper: true, - skipInheritedLayouts: true, -}; - -export default defineRoute((req, ctx) => { - return ( - -

updated

- -
- ); -}); diff --git a/tests/fixture_partials/routes/island_props_signals/index.tsx b/tests/fixture_partials/routes/island_props_signals/index.tsx deleted file mode 100644 index a6fff773faf..00000000000 --- a/tests/fixture_partials/routes/island_props_signals/index.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { Partial } from "$fresh/runtime.ts"; -import { useSignal } from "@preact/signals"; -import { Fader } from "../../islands/Fader.tsx"; -import SignalProp from "../../islands/SignalProp.tsx"; - -export default function PropsDemo() { - const sig = useSignal(0); - - return ( -
- - -

initial

- -
-
-

- - Update - -

-
- ); -} diff --git a/tests/fixture_partials/routes/island_props_signals/injected.tsx b/tests/fixture_partials/routes/island_props_signals/injected.tsx deleted file mode 100644 index 3ebbce74784..00000000000 --- a/tests/fixture_partials/routes/island_props_signals/injected.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { defineRoute } from "$fresh/src/server/defines.ts"; - -export default defineRoute(() => { - return new Response("", { - status: 302, - headers: { - Location: "/island_props_signals", - }, - }); -}); diff --git a/tests/fixture_partials/routes/island_props_signals/partial.tsx b/tests/fixture_partials/routes/island_props_signals/partial.tsx deleted file mode 100644 index 80761842545..00000000000 --- a/tests/fixture_partials/routes/island_props_signals/partial.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { defineRoute } from "$fresh/src/server/defines.ts"; -import { RouteConfig } from "$fresh/server.ts"; -import { Partial } from "$fresh/runtime.ts"; -import { Fader } from "../../islands/Fader.tsx"; -import SignalProp from "../../islands/SignalProp.tsx"; -import { signal } from "@preact/signals"; - -export const config: RouteConfig = { - skipAppWrapper: true, - skipInheritedLayouts: true, -}; - -export default defineRoute((req, ctx) => { - const sig = signal(0); - return ( - - -

update

- -
-
- ); -}); diff --git a/tests/fixture_partials/routes/keys/index.tsx b/tests/fixture_partials/routes/keys/index.tsx deleted file mode 100644 index 85e4d6d3528..00000000000 --- a/tests/fixture_partials/routes/keys/index.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { Partial } from "$fresh/runtime.ts"; -import { Fader } from "../../islands/Fader.tsx"; -import Stateful from "../../islands/Stateful.tsx"; - -export default function SlotDemo() { - return ( -
- - -

Initial content

- {[ - , - , - , - ]} -
-
-

- - swap - -

-
- ); -} diff --git a/tests/fixture_partials/routes/keys/injected.tsx b/tests/fixture_partials/routes/keys/injected.tsx deleted file mode 100644 index a3de161fd67..00000000000 --- a/tests/fixture_partials/routes/keys/injected.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { defineRoute } from "$fresh/src/server/defines.ts"; - -export default defineRoute(() => { - return new Response("", { - status: 302, - headers: { - Location: "/keys", - }, - }); -}); diff --git a/tests/fixture_partials/routes/keys/swap.tsx b/tests/fixture_partials/routes/keys/swap.tsx deleted file mode 100644 index b1c179e2f2e..00000000000 --- a/tests/fixture_partials/routes/keys/swap.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { defineRoute } from "$fresh/src/server/defines.ts"; -import { RouteConfig } from "$fresh/server.ts"; -import { Partial } from "$fresh/runtime.ts"; -import { Fader } from "../../islands/Fader.tsx"; -import Stateful from "../../islands/Stateful.tsx"; - -export const config: RouteConfig = { - skipAppWrapper: true, - skipInheritedLayouts: true, -}; - -export default defineRoute((req, ctx) => { - return ( - - -

swapped content

- {[ - , - , - , - ]} -
-
- ); -}); diff --git a/tests/fixture_partials/routes/keys_components/index.tsx b/tests/fixture_partials/routes/keys_components/index.tsx deleted file mode 100644 index af0a7c7616e..00000000000 --- a/tests/fixture_partials/routes/keys_components/index.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { Partial } from "$fresh/runtime.ts"; -import { Fader } from "../../islands/Fader.tsx"; -import Stateful from "../../islands/Stateful.tsx"; -import { Keyed } from "../../components/Keyed.tsx"; - -export default function SlotDemo() { - return ( -
- - -

Initial content

- {[ - - - , - - - , - - - , - ]} -
-
-

- - swap - -

-
- ); -} diff --git a/tests/fixture_partials/routes/keys_components/injected.tsx b/tests/fixture_partials/routes/keys_components/injected.tsx deleted file mode 100644 index fd804e21933..00000000000 --- a/tests/fixture_partials/routes/keys_components/injected.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { defineRoute } from "$fresh/src/server/defines.ts"; - -export default defineRoute(() => { - return new Response("", { - status: 302, - headers: { - Location: "/keys_components", - }, - }); -}); diff --git a/tests/fixture_partials/routes/keys_components/swap.tsx b/tests/fixture_partials/routes/keys_components/swap.tsx deleted file mode 100644 index 6fe2892e028..00000000000 --- a/tests/fixture_partials/routes/keys_components/swap.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { defineRoute } from "$fresh/src/server/defines.ts"; -import { RouteConfig } from "$fresh/server.ts"; -import { Partial } from "$fresh/runtime.ts"; -import { Fader } from "../../islands/Fader.tsx"; -import Stateful from "../../islands/Stateful.tsx"; -import { Keyed } from "../../components/Keyed.tsx"; - -export const config: RouteConfig = { - skipAppWrapper: true, - skipInheritedLayouts: true, -}; - -export default defineRoute((req, ctx) => { - return ( - - -

swapped content

- {[ - - - , - - - , - - - , - ]} -
-
- ); -}); diff --git a/tests/fixture_partials/routes/keys_confusion/index.tsx b/tests/fixture_partials/routes/keys_confusion/index.tsx deleted file mode 100644 index cb9701f5989..00000000000 --- a/tests/fixture_partials/routes/keys_confusion/index.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import KeyExplorer from "../../islands/KeyExplorer.tsx"; - -export default function SlotDemo() { - return ; -} diff --git a/tests/fixture_partials/routes/keys_dom/index.tsx b/tests/fixture_partials/routes/keys_dom/index.tsx deleted file mode 100644 index 5cb5b2bf19c..00000000000 --- a/tests/fixture_partials/routes/keys_dom/index.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { Partial } from "$fresh/runtime.ts"; -import { Fader } from "../../islands/Fader.tsx"; -import Stateful from "../../islands/Stateful.tsx"; - -export default function SlotDemo() { - return ( -
- - -

Initial content

-
    - {[ -
  • - -
  • , -
  • - -
  • , -
  • - -
  • , - ]} -
-
-
-

- - swap - -

-
- ); -} diff --git a/tests/fixture_partials/routes/keys_dom/injected.tsx b/tests/fixture_partials/routes/keys_dom/injected.tsx deleted file mode 100644 index 400eb666290..00000000000 --- a/tests/fixture_partials/routes/keys_dom/injected.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { defineRoute } from "$fresh/src/server/defines.ts"; - -export default defineRoute(() => { - return new Response("", { - status: 302, - headers: { - Location: "/keys_dom", - }, - }); -}); diff --git a/tests/fixture_partials/routes/keys_dom/swap.tsx b/tests/fixture_partials/routes/keys_dom/swap.tsx deleted file mode 100644 index 61ea19c6048..00000000000 --- a/tests/fixture_partials/routes/keys_dom/swap.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { defineRoute } from "$fresh/src/server/defines.ts"; -import { RouteConfig } from "$fresh/server.ts"; -import { Partial } from "$fresh/runtime.ts"; -import { Fader } from "../../islands/Fader.tsx"; -import Stateful from "../../islands/Stateful.tsx"; - -export const config: RouteConfig = { - skipAppWrapper: true, - skipInheritedLayouts: true, -}; - -export default defineRoute((req, ctx) => { - return ( - - -

swapped content

-
    - {[ -
  • - -
  • , -
  • - -
  • , -
  • - -
  • , - ]} -
-
-
- ); -}); diff --git a/tests/fixture_partials/routes/keys_outside/index.tsx b/tests/fixture_partials/routes/keys_outside/index.tsx deleted file mode 100644 index 24565b3038b..00000000000 --- a/tests/fixture_partials/routes/keys_outside/index.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { ComponentChildren } from "preact"; - -export default function SlotDemo() { - return ( -
- A - B - C -
- ); -} - -function Foo(props: { children?: ComponentChildren }) { - return

{props.children}

; -} diff --git a/tests/fixture_partials/routes/loading/index.tsx b/tests/fixture_partials/routes/loading/index.tsx deleted file mode 100644 index b5ea8afea0d..00000000000 --- a/tests/fixture_partials/routes/loading/index.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { Partial } from "$fresh/runtime.ts"; -import { useSignal } from "@preact/signals"; -import Spinner from "../../islands/Spinner.tsx"; -import PartialTrigger from "../../islands/PartialTrigger.tsx"; - -export default function SlotDemo() { - const sig = useSignal(false); - return ( -
-
- -

Default content

- -
-
- - - update - -
- - partial trigger - -
- ); -} diff --git a/tests/fixture_partials/routes/loading/injected.tsx b/tests/fixture_partials/routes/loading/injected.tsx deleted file mode 100644 index 366b3bf7135..00000000000 --- a/tests/fixture_partials/routes/loading/injected.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { defineRoute } from "$fresh/src/server/defines.ts"; - -export default defineRoute(() => { - return new Response("", { - status: 302, - headers: { - Location: "/loading", - }, - }); -}); diff --git a/tests/fixture_partials/routes/loading/update.tsx b/tests/fixture_partials/routes/loading/update.tsx deleted file mode 100644 index 78e74f52877..00000000000 --- a/tests/fixture_partials/routes/loading/update.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { defineRoute } from "$fresh/src/server/defines.ts"; -import { RouteConfig } from "$fresh/server.ts"; -import { Partial } from "$fresh/runtime.ts"; -import { delay } from "../../../deps.ts"; - -export const config: RouteConfig = { - skipAppWrapper: true, - skipInheritedLayouts: true, -}; - -export default defineRoute(async (req, ctx) => { - // A bit of artificial delay to show the loader - await delay(200); - - return ( - -

it works

-
- ); -}); diff --git a/tests/fixture_partials/routes/missing_partial/index.tsx b/tests/fixture_partials/routes/missing_partial/index.tsx deleted file mode 100644 index beed883dc9c..00000000000 --- a/tests/fixture_partials/routes/missing_partial/index.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { Partial } from "$fresh/runtime.ts"; -import CounterA from "../../islands/CounterA.tsx"; -import { Fader } from "../../islands/Fader.tsx"; - -export default function WarnDemo() { - return ( -
- - -

Initial content

- -
-
-

- - update - -

-
- ); -} diff --git a/tests/fixture_partials/routes/missing_partial/injected.tsx b/tests/fixture_partials/routes/missing_partial/injected.tsx deleted file mode 100644 index e8cee1f6a7a..00000000000 --- a/tests/fixture_partials/routes/missing_partial/injected.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { defineRoute } from "$fresh/src/server/defines.ts"; - -export default defineRoute(() => { - return new Response("", { - status: 302, - headers: { - Location: "/missing_partial", - }, - }); -}); diff --git a/tests/fixture_partials/routes/missing_partial/update.tsx b/tests/fixture_partials/routes/missing_partial/update.tsx deleted file mode 100644 index 6b8e1f171b0..00000000000 --- a/tests/fixture_partials/routes/missing_partial/update.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { defineRoute } from "$fresh/src/server/defines.ts"; -import { RouteConfig } from "$fresh/server.ts"; -import { Partial } from "$fresh/runtime.ts"; - -export const config: RouteConfig = { - skipAppWrapper: true, - skipInheritedLayouts: true, -}; - -export default defineRoute((req, ctx) => { - return ( - -

append content

-
- ); -}); diff --git a/tests/fixture_partials/routes/mode/append.tsx b/tests/fixture_partials/routes/mode/append.tsx deleted file mode 100644 index 8f8e023f514..00000000000 --- a/tests/fixture_partials/routes/mode/append.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { defineRoute } from "$fresh/src/server/defines.ts"; -import { RouteConfig } from "$fresh/server.ts"; -import { Partial } from "$fresh/runtime.ts"; -import { Fader } from "../../islands/Fader.tsx"; -import Other from "../../islands/Other.tsx"; - -export const config: RouteConfig = { - skipAppWrapper: true, - skipInheritedLayouts: true, -}; - -export default defineRoute((req, ctx) => { - return ( - - -

append content

- -
-
- ); -}); diff --git a/tests/fixture_partials/routes/mode/index.tsx b/tests/fixture_partials/routes/mode/index.tsx deleted file mode 100644 index 951ce9a7af0..00000000000 --- a/tests/fixture_partials/routes/mode/index.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import { Partial } from "$fresh/runtime.ts"; -import CounterA from "../../islands/CounterA.tsx"; -import { Fader } from "../../islands/Fader.tsx"; - -export default function ModeDemo() { - return ( -
- - -

Initial content

- -
-
-

- - replace - -

-

- - append - -

-

- - prepend - -

- -
-    
- ); -} diff --git a/tests/fixture_partials/routes/mode/injected.tsx b/tests/fixture_partials/routes/mode/injected.tsx deleted file mode 100644 index 860ef429f7f..00000000000 --- a/tests/fixture_partials/routes/mode/injected.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { defineRoute } from "$fresh/src/server/defines.ts"; - -export default defineRoute(() => { - return new Response("", { - status: 302, - headers: { - Location: "/mode", - }, - }); -}); diff --git a/tests/fixture_partials/routes/mode/prepend.tsx b/tests/fixture_partials/routes/mode/prepend.tsx deleted file mode 100644 index 5596a943fce..00000000000 --- a/tests/fixture_partials/routes/mode/prepend.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { defineRoute } from "$fresh/src/server/defines.ts"; -import { RouteConfig } from "$fresh/server.ts"; -import { Partial } from "$fresh/runtime.ts"; -import { Fader } from "../../islands/Fader.tsx"; - -export const config: RouteConfig = { - skipAppWrapper: true, - skipInheritedLayouts: true, -}; - -export default defineRoute((req, ctx) => { - return ( - - -

prepend

-

prepend content

-
-
- ); -}); diff --git a/tests/fixture_partials/routes/mode/replace.tsx b/tests/fixture_partials/routes/mode/replace.tsx deleted file mode 100644 index 55a7ddc8856..00000000000 --- a/tests/fixture_partials/routes/mode/replace.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { defineRoute } from "$fresh/src/server/defines.ts"; -import { RouteConfig } from "$fresh/server.ts"; -import { Partial } from "$fresh/runtime.ts"; -import { Fader } from "../../islands/Fader.tsx"; - -export const config: RouteConfig = { - skipAppWrapper: true, - skipInheritedLayouts: true, -}; - -export default defineRoute((req, ctx) => { - return ( - - -

replace

-
-
- ); -}); diff --git a/tests/fixture_partials/routes/nested/index.tsx b/tests/fixture_partials/routes/nested/index.tsx deleted file mode 100644 index 4b22319be68..00000000000 --- a/tests/fixture_partials/routes/nested/index.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { Partial } from "$fresh/runtime.ts"; -import { ComponentChildren } from "preact"; -import { Fader } from "../../islands/Fader.tsx"; -import { Logger } from "../../islands/Logger.tsx"; - -export function Inner() { - return ( - - - -

inner

-
-
-
- ); -} - -function Outer({ children }: { children: ComponentChildren }) { - return ( - - - -

outer

- - {children} -
-
-
- ); -} - -export default function SlotDemo() { - return ( -
- - - -

- - -

-
-    
- ); -} diff --git a/tests/fixture_partials/routes/nested/inner.tsx b/tests/fixture_partials/routes/nested/inner.tsx deleted file mode 100644 index 0f1971243eb..00000000000 --- a/tests/fixture_partials/routes/nested/inner.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { defineRoute } from "$fresh/src/server/defines.ts"; -import { RouteConfig } from "$fresh/server.ts"; -import { Partial } from "$fresh/runtime.ts"; -import { Fader } from "../../islands/Fader.tsx"; -import { Logger } from "../../islands/Logger.tsx"; - -export const config: RouteConfig = { - skipAppWrapper: true, - skipInheritedLayouts: true, -}; - -export default defineRoute(() => ( -
-
- - - -

updated inner

-
-
-
-
-
-)); diff --git a/tests/fixture_partials/routes/nested/outer.tsx b/tests/fixture_partials/routes/nested/outer.tsx deleted file mode 100644 index f604c994eec..00000000000 --- a/tests/fixture_partials/routes/nested/outer.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { defineRoute } from "$fresh/src/server/defines.ts"; -import { RouteConfig } from "$fresh/server.ts"; -import { Partial } from "$fresh/runtime.ts"; -import { Inner } from "./index.tsx"; -import { Fader } from "../../islands/Fader.tsx"; -import { Logger } from "../../islands/Logger.tsx"; - -export const config: RouteConfig = { - skipAppWrapper: true, - skipInheritedLayouts: true, -}; - -export default defineRoute(() => ( -
-
- - - -

updated outer

- - -
-
-
-
-
-)); diff --git a/tests/fixture_partials/routes/no_islands/index.tsx b/tests/fixture_partials/routes/no_islands/index.tsx deleted file mode 100644 index 05832034a38..00000000000 --- a/tests/fixture_partials/routes/no_islands/index.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { Partial } from "$fresh/runtime.ts"; - -export default function SlotDemo() { - return ( -
-
- -

Default content

-
-
- - update - -
- ); -} diff --git a/tests/fixture_partials/routes/no_islands/injected.tsx b/tests/fixture_partials/routes/no_islands/injected.tsx deleted file mode 100644 index ef04413dd33..00000000000 --- a/tests/fixture_partials/routes/no_islands/injected.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { defineRoute } from "$fresh/src/server/defines.ts"; - -export default defineRoute(() => { - return new Response("", { - status: 302, - headers: { - Location: "/no_islands", - }, - }); -}); diff --git a/tests/fixture_partials/routes/no_islands/update.tsx b/tests/fixture_partials/routes/no_islands/update.tsx deleted file mode 100644 index e8a01eb596b..00000000000 --- a/tests/fixture_partials/routes/no_islands/update.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { defineRoute } from "$fresh/src/server/defines.ts"; -import { RouteConfig } from "$fresh/server.ts"; -import { Partial } from "$fresh/runtime.ts"; - -export const config: RouteConfig = { - skipAppWrapper: true, - skipInheritedLayouts: true, -}; - -export default defineRoute((req, ctx) => { - return ( - -

it works

-
- ); -}); diff --git a/tests/fixture_partials/routes/no_partial_response/index.tsx b/tests/fixture_partials/routes/no_partial_response/index.tsx deleted file mode 100644 index 7d76e8b5252..00000000000 --- a/tests/fixture_partials/routes/no_partial_response/index.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { Partial } from "$fresh/runtime.ts"; -import CounterA from "../../islands/CounterA.tsx"; -import { Fader } from "../../islands/Fader.tsx"; - -export default function WarnDemo() { - return ( -
- - -

Initial content

- -
-
-

- - update - -

-

-    
- ); -} diff --git a/tests/fixture_partials/routes/no_partial_response/injected.tsx b/tests/fixture_partials/routes/no_partial_response/injected.tsx deleted file mode 100644 index 7606f2f69c3..00000000000 --- a/tests/fixture_partials/routes/no_partial_response/injected.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { defineRoute } from "$fresh/src/server/defines.ts"; - -export default defineRoute(() => { - return new Response("", { - status: 302, - headers: { - Location: "/no_partial_response", - }, - }); -}); diff --git a/tests/fixture_partials/routes/no_partial_response/update.tsx b/tests/fixture_partials/routes/no_partial_response/update.tsx deleted file mode 100644 index 48077539bbf..00000000000 --- a/tests/fixture_partials/routes/no_partial_response/update.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { defineRoute } from "$fresh/src/server/defines.ts"; -import { RouteConfig } from "$fresh/server.ts"; - -export const config: RouteConfig = { - skipAppWrapper: true, - skipInheritedLayouts: true, -}; - -export default defineRoute((req, ctx) => { - return

append content

; -}); diff --git a/tests/fixture_partials/routes/partial_slot_inside_island.tsx b/tests/fixture_partials/routes/partial_slot_inside_island.tsx deleted file mode 100644 index 17f5b3fc4ca..00000000000 --- a/tests/fixture_partials/routes/partial_slot_inside_island.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import InvalidSlot from "../islands/InvalidSlot.tsx"; - -export default function SlotDemo() { - return ; -} diff --git a/tests/fixture_partials/routes/redirected/handler.tsx b/tests/fixture_partials/routes/redirected/handler.tsx deleted file mode 100644 index d4eeb6044e3..00000000000 --- a/tests/fixture_partials/routes/redirected/handler.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { RouteConfig } from "$fresh/server.ts"; -import { Partial } from "$fresh/runtime.ts"; - -export const config: RouteConfig = { - skipAppWrapper: true, - skipInheritedLayouts: true, -}; - -export default function Home() { - return ( - -

Updated content

-
- ); -} diff --git a/tests/fixture_partials/routes/redirected/index.tsx b/tests/fixture_partials/routes/redirected/index.tsx deleted file mode 100644 index e28f36aa445..00000000000 --- a/tests/fixture_partials/routes/redirected/index.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { Partial } from "$fresh/runtime.ts"; - -export default function SlotDemo() { - return ( -
-
- -

Default content

-
-
- - update - -
- ); -} diff --git a/tests/fixture_partials/routes/redirected/injected.tsx b/tests/fixture_partials/routes/redirected/injected.tsx deleted file mode 100644 index 43db45a9944..00000000000 --- a/tests/fixture_partials/routes/redirected/injected.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { defineRoute } from "$fresh/src/server/defines.ts"; - -export default defineRoute(() => { - return new Response("", { - status: 302, - headers: { - Location: "/redirected", - }, - }); -}); diff --git a/tests/fixture_partials/routes/redirected/redirect.tsx b/tests/fixture_partials/routes/redirected/redirect.tsx deleted file mode 100644 index dae94d280c5..00000000000 --- a/tests/fixture_partials/routes/redirected/redirect.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { Handler } from "$fresh/server.ts"; - -export const handler: Handler = (req) => { - return new Response(null, { - headers: { - ...req.headers, - Location: "/redirected/handler", - }, - status: 307, - }); -}; diff --git a/tests/fixture_partials/routes/relative_link/index.tsx b/tests/fixture_partials/routes/relative_link/index.tsx deleted file mode 100644 index 81a428b92aa..00000000000 --- a/tests/fixture_partials/routes/relative_link/index.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { Partial } from "$fresh/runtime.ts"; -import { defineRoute } from "$fresh/src/server/defines.ts"; -import { Fader } from "../../islands/Fader.tsx"; - -export default defineRoute((req) => { - const url = new URL(req.url); - - return ( -
- - -

- {url.searchParams.has("refresh") - ? "Refreshed content" - : "Initial content"} -

-
-
-

- -

-
- ); -}); diff --git a/tests/fixture_partials/routes/scroll_restoration/index.tsx b/tests/fixture_partials/routes/scroll_restoration/index.tsx deleted file mode 100644 index d46fc6d21f8..00000000000 --- a/tests/fixture_partials/routes/scroll_restoration/index.tsx +++ /dev/null @@ -1,300 +0,0 @@ -import { Partial } from "$fresh/runtime.ts"; -import { Fader } from "../../islands/Fader.tsx"; - -export default function ModeDemo() { - return ( -
- - -

Initial content

-

- Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean - commodo ligula eget dolor. Aenean massa. Cum sociis natoque - penatibus et magnis dis parturient montes, nascetur ridiculus mus. - Donec quam felis, ultricies nec, pellentesque eu, pretium quis, sem. - Nulla consequat massa quis enim. Donec pede justo, fringilla vel, - aliquet nec, vulputate eget, arcu. In enim justo, rhoncus ut, - imperdiet a, venenatis vitae, justo. Nullam dictum felis eu pede - mollis pretium. Integer tincidunt. Cras dapibus. Vivamus elementum - semper nisi. Aenean vulputate eleifend tellus. Aenean leo ligula, - porttitor eu, consequat vitae, eleifend ac, enim. Aliquam lorem - ante, dapibus in, viverra quis, feugiat a, tellus. Phasellus viverra - nulla ut metus varius laoreet. Quisque rutrum. Aenean imperdiet. - Etiam ultricies nisi vel augue. Curabitur ullamcorper ultricies - nisi. Nam eget dui. Etiam rhoncus. Maecenas tempus, tellus eget - condimentum rhoncus, sem quam semper libero, sit amet adipiscing sem - neque sed ipsum. Nam quam nunc, blandit vel, luctus pulvinar, - hendrerit id, lorem. Maecenas nec odio et ante tincidunt tempus. - Donec vitae sapien ut libero venenatis faucibus. Nullam quis ante. - Etiam sit amet orci eget eros faucibus tincidunt. Duis leo. Sed - fringilla mauris sit amet nibh. Donec sodales sagittis magna. Sed - consequat, leo eget bibendum sodales, augue velit cursus nunc, -

-

- Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean - commodo ligula eget dolor. Aenean massa. Cum sociis natoque - penatibus et magnis dis parturient montes, nascetur ridiculus mus. - Donec quam felis, ultricies nec, pellentesque eu, pretium quis, sem. - Nulla consequat massa quis enim. Donec pede justo, fringilla vel, - aliquet nec, vulputate eget, arcu. In enim justo, rhoncus ut, - imperdiet a, venenatis vitae, justo. Nullam dictum felis eu pede - mollis pretium. Integer tincidunt. Cras dapibus. Vivamus elementum - semper nisi. Aenean vulputate eleifend tellus. Aenean leo ligula, - porttitor eu, consequat vitae, eleifend ac, enim. Aliquam lorem - ante, dapibus in, viverra quis, feugiat a, tellus. Phasellus viverra - nulla ut metus varius laoreet. Quisque rutrum. Aenean imperdiet. - Etiam ultricies nisi vel augue. Curabitur ullamcorper ultricies - nisi. Nam eget dui. Etiam rhoncus. Maecenas tempus, tellus eget - condimentum rhoncus, sem quam semper libero, sit amet adipiscing sem - neque sed ipsum. Nam quam nunc, blandit vel, luctus pulvinar, - hendrerit id, lorem. Maecenas nec odio et ante tincidunt tempus. - Donec vitae sapien ut libero venenatis faucibus. Nullam quis ante. - Etiam sit amet orci eget eros faucibus tincidunt. Duis leo. Sed - fringilla mauris sit amet nibh. Donec sodales sagittis magna. Sed - consequat, leo eget bibendum sodales, augue velit cursus nunc, -

-

- Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean - commodo ligula eget dolor. Aenean massa. Cum sociis natoque - penatibus et magnis dis parturient montes, nascetur ridiculus mus. - Donec quam felis, ultricies nec, pellentesque eu, pretium quis, sem. - Nulla consequat massa quis enim. Donec pede justo, fringilla vel, - aliquet nec, vulputate eget, arcu. In enim justo, rhoncus ut, - imperdiet a, venenatis vitae, justo. Nullam dictum felis eu pede - mollis pretium. Integer tincidunt. Cras dapibus. Vivamus elementum - semper nisi. Aenean vulputate eleifend tellus. Aenean leo ligula, - porttitor eu, consequat vitae, eleifend ac, enim. Aliquam lorem - ante, dapibus in, viverra quis, feugiat a, tellus. Phasellus viverra - nulla ut metus varius laoreet. Quisque rutrum. Aenean imperdiet. - Etiam ultricies nisi vel augue. Curabitur ullamcorper ultricies - nisi. Nam eget dui. Etiam rhoncus. Maecenas tempus, tellus eget - condimentum rhoncus, sem quam semper libero, sit amet adipiscing sem - neque sed ipsum. Nam quam nunc, blandit vel, luctus pulvinar, - hendrerit id, lorem. Maecenas nec odio et ante tincidunt tempus. - Donec vitae sapien ut libero venenatis faucibus. Nullam quis ante. - Etiam sit amet orci eget eros faucibus tincidunt. Duis leo. Sed - fringilla mauris sit amet nibh. Donec sodales sagittis magna. Sed - consequat, leo eget bibendum sodales, augue velit cursus nunc, -

-

- Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean - commodo ligula eget dolor. Aenean massa. Cum sociis natoque - penatibus et magnis dis parturient montes, nascetur ridiculus mus. - Donec quam felis, ultricies nec, pellentesque eu, pretium quis, sem. - Nulla consequat massa quis enim. Donec pede justo, fringilla vel, - aliquet nec, vulputate eget, arcu. In enim justo, rhoncus ut, - imperdiet a, venenatis vitae, justo. Nullam dictum felis eu pede - mollis pretium. Integer tincidunt. Cras dapibus. Vivamus elementum - semper nisi. Aenean vulputate eleifend tellus. Aenean leo ligula, - porttitor eu, consequat vitae, eleifend ac, enim. Aliquam lorem - ante, dapibus in, viverra quis, feugiat a, tellus. Phasellus viverra - nulla ut metus varius laoreet. Quisque rutrum. Aenean imperdiet. - Etiam ultricies nisi vel augue. Curabitur ullamcorper ultricies - nisi. Nam eget dui. Etiam rhoncus. Maecenas tempus, tellus eget - condimentum rhoncus, sem quam semper libero, sit amet adipiscing sem - neque sed ipsum. Nam quam nunc, blandit vel, luctus pulvinar, - hendrerit id, lorem. Maecenas nec odio et ante tincidunt tempus. - Donec vitae sapien ut libero venenatis faucibus. Nullam quis ante. - Etiam sit amet orci eget eros faucibus tincidunt. Duis leo. Sed - fringilla mauris sit amet nibh. Donec sodales sagittis magna. Sed - consequat, leo eget bibendum sodales, augue velit cursus nunc, -

-

- Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean - commodo ligula eget dolor. Aenean massa. Cum sociis natoque - penatibus et magnis dis parturient montes, nascetur ridiculus mus. - Donec quam felis, ultricies nec, pellentesque eu, pretium quis, sem. - Nulla consequat massa quis enim. Donec pede justo, fringilla vel, - aliquet nec, vulputate eget, arcu. In enim justo, rhoncus ut, - imperdiet a, venenatis vitae, justo. Nullam dictum felis eu pede - mollis pretium. Integer tincidunt. Cras dapibus. Vivamus elementum - semper nisi. Aenean vulputate eleifend tellus. Aenean leo ligula, - porttitor eu, consequat vitae, eleifend ac, enim. Aliquam lorem - ante, dapibus in, viverra quis, feugiat a, tellus. Phasellus viverra - nulla ut metus varius laoreet. Quisque rutrum. Aenean imperdiet. - Etiam ultricies nisi vel augue. Curabitur ullamcorper ultricies - nisi. Nam eget dui. Etiam rhoncus. Maecenas tempus, tellus eget - condimentum rhoncus, sem quam semper libero, sit amet adipiscing sem - neque sed ipsum. Nam quam nunc, blandit vel, luctus pulvinar, - hendrerit id, lorem. Maecenas nec odio et ante tincidunt tempus. - Donec vitae sapien ut libero venenatis faucibus. Nullam quis ante. - Etiam sit amet orci eget eros faucibus tincidunt. Duis leo. Sed - fringilla mauris sit amet nibh. Donec sodales sagittis magna. Sed - consequat, leo eget bibendum sodales, augue velit cursus nunc, -

-

- Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean - commodo ligula eget dolor. Aenean massa. Cum sociis natoque - penatibus et magnis dis parturient montes, nascetur ridiculus mus. - Donec quam felis, ultricies nec, pellentesque eu, pretium quis, sem. - Nulla consequat massa quis enim. Donec pede justo, fringilla vel, - aliquet nec, vulputate eget, arcu. In enim justo, rhoncus ut, - imperdiet a, venenatis vitae, justo. Nullam dictum felis eu pede - mollis pretium. Integer tincidunt. Cras dapibus. Vivamus elementum - semper nisi. Aenean vulputate eleifend tellus. Aenean leo ligula, - porttitor eu, consequat vitae, eleifend ac, enim. Aliquam lorem - ante, dapibus in, viverra quis, feugiat a, tellus. Phasellus viverra - nulla ut metus varius laoreet. Quisque rutrum. Aenean imperdiet. - Etiam ultricies nisi vel augue. Curabitur ullamcorper ultricies - nisi. Nam eget dui. Etiam rhoncus. Maecenas tempus, tellus eget - condimentum rhoncus, sem quam semper libero, sit amet adipiscing sem - neque sed ipsum. Nam quam nunc, blandit vel, luctus pulvinar, - hendrerit id, lorem. Maecenas nec odio et ante tincidunt tempus. - Donec vitae sapien ut libero venenatis faucibus. Nullam quis ante. - Etiam sit amet orci eget eros faucibus tincidunt. Duis leo. Sed - fringilla mauris sit amet nibh. Donec sodales sagittis magna. Sed - consequat, leo eget bibendum sodales, augue velit cursus nunc, -

-

- Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean - commodo ligula eget dolor. Aenean massa. Cum sociis natoque - penatibus et magnis dis parturient montes, nascetur ridiculus mus. - Donec quam felis, ultricies nec, pellentesque eu, pretium quis, sem. - Nulla consequat massa quis enim. Donec pede justo, fringilla vel, - aliquet nec, vulputate eget, arcu. In enim justo, rhoncus ut, - imperdiet a, venenatis vitae, justo. Nullam dictum felis eu pede - mollis pretium. Integer tincidunt. Cras dapibus. Vivamus elementum - semper nisi. Aenean vulputate eleifend tellus. Aenean leo ligula, - porttitor eu, consequat vitae, eleifend ac, enim. Aliquam lorem - ante, dapibus in, viverra quis, feugiat a, tellus. Phasellus viverra - nulla ut metus varius laoreet. Quisque rutrum. Aenean imperdiet. - Etiam ultricies nisi vel augue. Curabitur ullamcorper ultricies - nisi. Nam eget dui. Etiam rhoncus. Maecenas tempus, tellus eget - condimentum rhoncus, sem quam semper libero, sit amet adipiscing sem - neque sed ipsum. Nam quam nunc, blandit vel, luctus pulvinar, - hendrerit id, lorem. Maecenas nec odio et ante tincidunt tempus. - Donec vitae sapien ut libero venenatis faucibus. Nullam quis ante. - Etiam sit amet orci eget eros faucibus tincidunt. Duis leo. Sed - fringilla mauris sit amet nibh. Donec sodales sagittis magna. Sed - consequat, leo eget bibendum sodales, augue velit cursus nunc, -

-

- Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean - commodo ligula eget dolor. Aenean massa. Cum sociis natoque - penatibus et magnis dis parturient montes, nascetur ridiculus mus. - Donec quam felis, ultricies nec, pellentesque eu, pretium quis, sem. - Nulla consequat massa quis enim. Donec pede justo, fringilla vel, - aliquet nec, vulputate eget, arcu. In enim justo, rhoncus ut, - imperdiet a, venenatis vitae, justo. Nullam dictum felis eu pede - mollis pretium. Integer tincidunt. Cras dapibus. Vivamus elementum - semper nisi. Aenean vulputate eleifend tellus. Aenean leo ligula, - porttitor eu, consequat vitae, eleifend ac, enim. Aliquam lorem - ante, dapibus in, viverra quis, feugiat a, tellus. Phasellus viverra - nulla ut metus varius laoreet. Quisque rutrum. Aenean imperdiet. - Etiam ultricies nisi vel augue. Curabitur ullamcorper ultricies - nisi. Nam eget dui. Etiam rhoncus. Maecenas tempus, tellus eget - condimentum rhoncus, sem quam semper libero, sit amet adipiscing sem - neque sed ipsum. Nam quam nunc, blandit vel, luctus pulvinar, - hendrerit id, lorem. Maecenas nec odio et ante tincidunt tempus. - Donec vitae sapien ut libero venenatis faucibus. Nullam quis ante. - Etiam sit amet orci eget eros faucibus tincidunt. Duis leo. Sed - fringilla mauris sit amet nibh. Donec sodales sagittis magna. Sed - consequat, leo eget bibendum sodales, augue velit cursus nunc, -

-

- Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean - commodo ligula eget dolor. Aenean massa. Cum sociis natoque - penatibus et magnis dis parturient montes, nascetur ridiculus mus. - Donec quam felis, ultricies nec, pellentesque eu, pretium quis, sem. - Nulla consequat massa quis enim. Donec pede justo, fringilla vel, - aliquet nec, vulputate eget, arcu. In enim justo, rhoncus ut, - imperdiet a, venenatis vitae, justo. Nullam dictum felis eu pede - mollis pretium. Integer tincidunt. Cras dapibus. Vivamus elementum - semper nisi. Aenean vulputate eleifend tellus. Aenean leo ligula, - porttitor eu, consequat vitae, eleifend ac, enim. Aliquam lorem - ante, dapibus in, viverra quis, feugiat a, tellus. Phasellus viverra - nulla ut metus varius laoreet. Quisque rutrum. Aenean imperdiet. - Etiam ultricies nisi vel augue. Curabitur ullamcorper ultricies - nisi. Nam eget dui. Etiam rhoncus. Maecenas tempus, tellus eget - condimentum rhoncus, sem quam semper libero, sit amet adipiscing sem - neque sed ipsum. Nam quam nunc, blandit vel, luctus pulvinar, - hendrerit id, lorem. Maecenas nec odio et ante tincidunt tempus. - Donec vitae sapien ut libero venenatis faucibus. Nullam quis ante. - Etiam sit amet orci eget eros faucibus tincidunt. Duis leo. Sed - fringilla mauris sit amet nibh. Donec sodales sagittis magna. Sed - consequat, leo eget bibendum sodales, augue velit cursus nunc, -

-

- Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean - commodo ligula eget dolor. Aenean massa. Cum sociis natoque - penatibus et magnis dis parturient montes, nascetur ridiculus mus. - Donec quam felis, ultricies nec, pellentesque eu, pretium quis, sem. - Nulla consequat massa quis enim. Donec pede justo, fringilla vel, - aliquet nec, vulputate eget, arcu. In enim justo, rhoncus ut, - imperdiet a, venenatis vitae, justo. Nullam dictum felis eu pede - mollis pretium. Integer tincidunt. Cras dapibus. Vivamus elementum - semper nisi. Aenean vulputate eleifend tellus. Aenean leo ligula, - porttitor eu, consequat vitae, eleifend ac, enim. Aliquam lorem - ante, dapibus in, viverra quis, feugiat a, tellus. Phasellus viverra - nulla ut metus varius laoreet. Quisque rutrum. Aenean imperdiet. - Etiam ultricies nisi vel augue. Curabitur ullamcorper ultricies - nisi. Nam eget dui. Etiam rhoncus. Maecenas tempus, tellus eget - condimentum rhoncus, sem quam semper libero, sit amet adipiscing sem - neque sed ipsum. Nam quam nunc, blandit vel, luctus pulvinar, - hendrerit id, lorem. Maecenas nec odio et ante tincidunt tempus. - Donec vitae sapien ut libero venenatis faucibus. Nullam quis ante. - Etiam sit amet orci eget eros faucibus tincidunt. Duis leo. Sed - fringilla mauris sit amet nibh. Donec sodales sagittis magna. Sed - consequat, leo eget bibendum sodales, augue velit cursus nunc, -

-

- Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean - commodo ligula eget dolor. Aenean massa. Cum sociis natoque - penatibus et magnis dis parturient montes, nascetur ridiculus mus. - Donec quam felis, ultricies nec, pellentesque eu, pretium quis, sem. - Nulla consequat massa quis enim. Donec pede justo, fringilla vel, - aliquet nec, vulputate eget, arcu. In enim justo, rhoncus ut, - imperdiet a, venenatis vitae, justo. Nullam dictum felis eu pede - mollis pretium. Integer tincidunt. Cras dapibus. Vivamus elementum - semper nisi. Aenean vulputate eleifend tellus. Aenean leo ligula, - porttitor eu, consequat vitae, eleifend ac, enim. Aliquam lorem - ante, dapibus in, viverra quis, feugiat a, tellus. Phasellus viverra - nulla ut metus varius laoreet. Quisque rutrum. Aenean imperdiet. - Etiam ultricies nisi vel augue. Curabitur ullamcorper ultricies - nisi. Nam eget dui. Etiam rhoncus. Maecenas tempus, tellus eget - condimentum rhoncus, sem quam semper libero, sit amet adipiscing sem - neque sed ipsum. Nam quam nunc, blandit vel, luctus pulvinar, - hendrerit id, lorem. Maecenas nec odio et ante tincidunt tempus. - Donec vitae sapien ut libero venenatis faucibus. Nullam quis ante. - Etiam sit amet orci eget eros faucibus tincidunt. Duis leo. Sed - fringilla mauris sit amet nibh. Donec sodales sagittis magna. Sed - consequat, leo eget bibendum sodales, augue velit cursus nunc, -

-

- Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean - commodo ligula eget dolor. Aenean massa. Cum sociis natoque - penatibus et magnis dis parturient montes, nascetur ridiculus mus. - Donec quam felis, ultricies nec, pellentesque eu, pretium quis, sem. - Nulla consequat massa quis enim. Donec pede justo, fringilla vel, - aliquet nec, vulputate eget, arcu. In enim justo, rhoncus ut, - imperdiet a, venenatis vitae, justo. Nullam dictum felis eu pede - mollis pretium. Integer tincidunt. Cras dapibus. Vivamus elementum - semper nisi. Aenean vulputate eleifend tellus. Aenean leo ligula, - porttitor eu, consequat vitae, eleifend ac, enim. Aliquam lorem - ante, dapibus in, viverra quis, feugiat a, tellus. Phasellus viverra - nulla ut metus varius laoreet. Quisque rutrum. Aenean imperdiet. - Etiam ultricies nisi vel augue. Curabitur ullamcorper ultricies - nisi. Nam eget dui. Etiam rhoncus. Maecenas tempus, tellus eget - condimentum rhoncus, sem quam semper libero, sit amet adipiscing sem - neque sed ipsum. Nam quam nunc, blandit vel, luctus pulvinar, - hendrerit id, lorem. Maecenas nec odio et ante tincidunt tempus. - Donec vitae sapien ut libero venenatis faucibus. Nullam quis ante. - Etiam sit amet orci eget eros faucibus tincidunt. Duis leo. Sed - fringilla mauris sit amet nibh. Donec sodales sagittis magna. Sed - consequat, leo eget bibendum sodales, augue velit cursus nunc, -

-
-
-

- - update - -

- -
-    
- ); -} diff --git a/tests/fixture_partials/routes/scroll_restoration/injected.tsx b/tests/fixture_partials/routes/scroll_restoration/injected.tsx deleted file mode 100644 index 793c730061e..00000000000 --- a/tests/fixture_partials/routes/scroll_restoration/injected.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { defineRoute } from "$fresh/src/server/defines.ts"; - -export default defineRoute(() => { - return new Response("", { - status: 302, - headers: { - Location: "/scroll_restoration", - }, - }); -}); diff --git a/tests/fixture_partials/routes/scroll_restoration/update.tsx b/tests/fixture_partials/routes/scroll_restoration/update.tsx deleted file mode 100644 index 34540680f92..00000000000 --- a/tests/fixture_partials/routes/scroll_restoration/update.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { defineRoute } from "$fresh/src/server/defines.ts"; -import { RouteConfig } from "$fresh/server.ts"; -import { Partial } from "$fresh/runtime.ts"; -import { Fader } from "../../islands/Fader.tsx"; - -export const config: RouteConfig = { - skipAppWrapper: true, - skipInheritedLayouts: true, -}; - -export default defineRoute((req, ctx) => { - return ( - - -

update

-
-
- ); -}); diff --git a/tests/fixture_partials/routes/spoof_state/index.tsx b/tests/fixture_partials/routes/spoof_state/index.tsx deleted file mode 100644 index 0ee8e71bce9..00000000000 --- a/tests/fixture_partials/routes/spoof_state/index.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { Partial } from "$fresh/runtime.ts"; - -export default function SerializePrototype() { - return ( -
- -

initial

-
- Update -
- ); -} diff --git a/tests/fixture_partials/routes/spoof_state/partial.tsx b/tests/fixture_partials/routes/spoof_state/partial.tsx deleted file mode 100644 index 8884e759a85..00000000000 --- a/tests/fixture_partials/routes/spoof_state/partial.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { Partial } from "$fresh/runtime.ts"; -import DangerousIsland from "../../islands/DangerousIsland.tsx"; - -export default function Res() { - return ( - - {.invalid.json}`} /> -

partial

-
- ); -} diff --git a/tests/fixture_partials/static/other.css b/tests/fixture_partials/static/other.css deleted file mode 100644 index adc68fa6a4d..00000000000 --- a/tests/fixture_partials/static/other.css +++ /dev/null @@ -1,3 +0,0 @@ -h1 { - color: red; -} diff --git a/tests/fixture_partials/static/style.css b/tests/fixture_partials/static/style.css deleted file mode 100644 index f60ef4f5b70..00000000000 --- a/tests/fixture_partials/static/style.css +++ /dev/null @@ -1,8 +0,0 @@ -@keyframes fade { - 0% { - background: white; - } - 100% { - background: peachpuff; - } -} diff --git a/tests/fixture_plugin/deno.json b/tests/fixture_plugin/deno.json deleted file mode 100644 index b211f1f4233..00000000000 --- a/tests/fixture_plugin/deno.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "lock": false, - "imports": { - "$fresh/": "../../", - "preact": "https://esm.sh/preact@10.22.0", - "preact/": "https://esm.sh/preact@10.22.0/" - }, - "compilerOptions": { - "jsx": "react-jsx", - "jsxImportSource": "preact" - } -} diff --git a/tests/fixture_plugin/dev.ts b/tests/fixture_plugin/dev.ts deleted file mode 100755 index 2d85d6c183c..00000000000 --- a/tests/fixture_plugin/dev.ts +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env -S deno run -A --watch=static/,routes/ - -import dev from "$fresh/dev.ts"; - -await dev(import.meta.url, "./main.ts"); diff --git a/tests/fixture_plugin/fresh.config.ts b/tests/fixture_plugin/fresh.config.ts deleted file mode 100644 index fd8baf31efa..00000000000 --- a/tests/fixture_plugin/fresh.config.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { FreshConfig } from "$fresh/server.ts"; -import cssInjectPlugin from "./utils/css-inject-plugin.ts"; -import jsInjectPlugin from "./utils/js-inject-plugin.ts"; -import cssInjectPluginAsync from "./utils/css-inject-plugin-async.ts"; -import linkInjectPlugin from "./utils/link-inject-plugin.ts"; -import routePlugin from "./utils/route-plugin.ts"; -import secondMiddlewarePlugin from "$fresh/tests/fixture_plugin/utils/second-middleware-plugin.ts"; - -export default { - plugins: [ - cssInjectPlugin, - jsInjectPlugin, - cssInjectPluginAsync, - linkInjectPlugin, - routePlugin({ title: "Title Set From Plugin Config", async: false }), - secondMiddlewarePlugin(), - ], -} as FreshConfig; diff --git a/tests/fixture_plugin/fresh.gen.ts b/tests/fixture_plugin/fresh.gen.ts deleted file mode 100644 index a4bc1b4e804..00000000000 --- a/tests/fixture_plugin/fresh.gen.ts +++ /dev/null @@ -1,25 +0,0 @@ -// DO NOT EDIT. This file is generated by Fresh. -// This file SHOULD be checked into source version control. -// This file is automatically updated during development when running `dev.ts`. - -import * as $lots_of_middleware_index from "./routes/lots-of-middleware/index.tsx"; -import * as $static from "./routes/static.tsx"; -import * as $test from "./routes/test.tsx"; -import * as $with_island from "./routes/with-island.tsx"; -import * as $Island from "./islands/Island.tsx"; -import { type Manifest } from "$fresh/server.ts"; - -const manifest = { - routes: { - "./routes/lots-of-middleware/index.tsx": $lots_of_middleware_index, - "./routes/static.tsx": $static, - "./routes/test.tsx": $test, - "./routes/with-island.tsx": $with_island, - }, - islands: { - "./islands/Island.tsx": $Island, - }, - baseUrl: import.meta.url, -} satisfies Manifest; - -export default manifest; diff --git a/tests/fixture_plugin/islands/Island.tsx b/tests/fixture_plugin/islands/Island.tsx deleted file mode 100644 index 73bcdcf3e9d..00000000000 --- a/tests/fixture_plugin/islands/Island.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { IS_BROWSER } from "../../../src/runtime/utils.ts"; - -export default function Island() { - const id = IS_BROWSER ? "csr" : "ssr"; - return ( -
-

{id}

-
- ); -} diff --git a/tests/fixture_plugin/main.ts b/tests/fixture_plugin/main.ts deleted file mode 100644 index fc9359215e3..00000000000 --- a/tests/fixture_plugin/main.ts +++ /dev/null @@ -1,11 +0,0 @@ -/// -/// -/// -/// -/// - -import { start } from "$fresh/server.ts"; -import manifest from "./fresh.gen.ts"; -import config from "./fresh.config.ts"; - -await start(manifest, config); diff --git a/tests/fixture_plugin/routes/lots-of-middleware/index.tsx b/tests/fixture_plugin/routes/lots-of-middleware/index.tsx deleted file mode 100644 index e154e8c15bb..00000000000 --- a/tests/fixture_plugin/routes/lots-of-middleware/index.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { Handlers, PageProps } from "$fresh/server.ts"; -import { PluginMiddlewareState } from "../../utils/route-plugin.ts"; - -export const handler: Handlers = { - async GET(_req, ctx) { - const resp = await ctx.render(); - return resp; - }, -}; - -export default function Home(props: PageProps) { - const value = props.state.num; - return ( -
-

{value}

-
- ); -} diff --git a/tests/fixture_plugin/routes/static.tsx b/tests/fixture_plugin/routes/static.tsx deleted file mode 100644 index 1fc383ff7f3..00000000000 --- a/tests/fixture_plugin/routes/static.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { inject } from "../utils/css-inject-plugin.ts"; - -export default function Home() { - inject("body { color: red; }"); - return ( -
-

Hello World

-
- ); -} diff --git a/tests/fixture_plugin/routes/test.tsx b/tests/fixture_plugin/routes/test.tsx deleted file mode 100644 index 2180db94460..00000000000 --- a/tests/fixture_plugin/routes/test.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { FreshContext, Handlers, PageProps } from "../../../server.ts"; - -export const handler: Handlers = { - async GET(_req, ctx: FreshContext<{ test: string }, unknown>) { - const resp = await ctx.render(); - return resp; - }, -}; - -export default function Home(props: PageProps) { - const value = props.state.test; - return ( -
-

{value}

-
- ); -} diff --git a/tests/fixture_plugin/routes/with-island.tsx b/tests/fixture_plugin/routes/with-island.tsx deleted file mode 100644 index e45ecc10889..00000000000 --- a/tests/fixture_plugin/routes/with-island.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import Island from "../islands/Island.tsx"; -import { inject } from "../utils/css-inject-plugin.ts"; - -export default function Home() { - inject("body { color: red; }"); - return ( -
-

Hello World

- -
- ); -} diff --git a/tests/fixture_plugin/static/print.css b/tests/fixture_plugin/static/print.css deleted file mode 100644 index 901f9301fc3..00000000000 --- a/tests/fixture_plugin/static/print.css +++ /dev/null @@ -1,3 +0,0 @@ -.body { - color: black; -} diff --git a/tests/fixture_plugin/static/styles.css b/tests/fixture_plugin/static/styles.css deleted file mode 100644 index b4197b16a19..00000000000 --- a/tests/fixture_plugin/static/styles.css +++ /dev/null @@ -1,3 +0,0 @@ -.body { - color: red; -} diff --git a/tests/fixture_plugin/utils/css-inject-plugin-async.ts b/tests/fixture_plugin/utils/css-inject-plugin-async.ts deleted file mode 100644 index 356ad1d4d13..00000000000 --- a/tests/fixture_plugin/utils/css-inject-plugin-async.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Plugin } from "$fresh/server.ts"; - -let CSS_TO_INJECT = "h1 { text-decoration: underline; }"; - -export default { - name: "css-inject-async", - async renderAsync(ctx) { - await new Promise((res) => setTimeout(res, 50)); - const res = await ctx.renderAsync(); - if (res.requiresHydration) { - CSS_TO_INJECT += " h1 { font-style: italic; }"; - } - return { styles: [{ cssText: CSS_TO_INJECT, id: "def" }] }; - }, -} as Plugin; diff --git a/tests/fixture_plugin/utils/css-inject-plugin.ts b/tests/fixture_plugin/utils/css-inject-plugin.ts deleted file mode 100644 index 6899d2b8784..00000000000 --- a/tests/fixture_plugin/utils/css-inject-plugin.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Plugin } from "$fresh/server.ts"; - -let CSS_TO_INJECT = ""; -export function inject(cssText: string) { - CSS_TO_INJECT = cssText; -} - -export default { - name: "css-inject", - render(ctx) { - CSS_TO_INJECT = ""; - const res = ctx.render(); - if (res.requiresHydration) { - CSS_TO_INJECT += " h1 { color: blue; }"; - } - return { styles: [{ cssText: CSS_TO_INJECT, id: "abc" }] }; - }, -} as Plugin; diff --git a/tests/fixture_plugin/utils/js-inject-main.ts b/tests/fixture_plugin/utils/js-inject-main.ts deleted file mode 100644 index 81423edee23..00000000000 --- a/tests/fixture_plugin/utils/js-inject-main.ts +++ /dev/null @@ -1,3 +0,0 @@ -export default function (state: string) { - document.title = state; -} diff --git a/tests/fixture_plugin/utils/js-inject-plugin.ts b/tests/fixture_plugin/utils/js-inject-plugin.ts deleted file mode 100644 index 9e9265c1fb3..00000000000 --- a/tests/fixture_plugin/utils/js-inject-plugin.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Plugin } from "$fresh/server.ts"; - -export default { - name: "js-inject", - entrypoints: { - "main": new URL("./js-inject-main.ts", import.meta.url).href, - }, - render(ctx) { - const res = ctx.render(); - if (res.requiresHydration) { - return { scripts: [{ entrypoint: "main", state: "JS injected!" }] }; - } - return {}; - }, -} as Plugin; diff --git a/tests/fixture_plugin/utils/link-inject-plugin.ts b/tests/fixture_plugin/utils/link-inject-plugin.ts deleted file mode 100644 index aecd1646ab7..00000000000 --- a/tests/fixture_plugin/utils/link-inject-plugin.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Plugin } from "$fresh/server.ts"; - -export default { - name: "link-inject", - render(ctx) { - ctx.render(); - return { - links: [{ rel: "stylesheet", href: "styles.css" }, { - rel: "stylesheet", - href: "print.css", - media: "print", - }], - }; - }, -} as Plugin; diff --git a/tests/fixture_plugin/utils/route-plugin.ts b/tests/fixture_plugin/utils/route-plugin.ts deleted file mode 100644 index c549bfe9b02..00000000000 --- a/tests/fixture_plugin/utils/route-plugin.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { FreshContext, Plugin } from "$fresh/server.ts"; -import { handler as testMiddleware } from "./sample_routes/_middleware.ts"; -import { AppBuilder } from "./sample_routes/AppBuilder.tsx"; -import IslandPluginComponent from "./sample_routes/PluginRouteWithIsland.tsx"; -import { SimpleRoute } from "./sample_routes/simple-route.tsx"; -import AsyncRoute from "./sample_routes/async-route.tsx"; -export type { Options }; - -interface Options { - title: string; - async: boolean; -} -export type PluginMiddlewareState = { - num: number; - test: string; -}; - -const twoPointlessMiddlewares = [ - async ( - _req: Request, - ctx: FreshContext, - ) => { - ctx.state.num = ctx.state.num === undefined ? 1 : ctx.state.num + 1; - return await ctx.next(); - }, - async ( - _req: Request, - ctx: FreshContext, - ) => { - ctx.state.num = ctx.state.num === undefined ? 1 : ctx.state.num + 1; - return await ctx.next(); - }, -]; - -export default function routePlugin( - options: Options, -): Plugin { - return { - name: "routePlugin", - middlewares: [{ - middleware: { handler: testMiddleware }, - path: "/", - }, { - middleware: { - handler: twoPointlessMiddlewares, - }, - path: "lots-of-middleware", - }], - routes: [ - { path: "/async-route", component: AsyncRoute }, - { - path: "/_app", - component: AppBuilder(options), - }, - { path: "no-leading-slash-here", component: SimpleRoute }, - { - path: "pluginroutewithisland", - component: IslandPluginComponent, - }, - ], - islands: { - baseLocation: import.meta.url, - paths: ["./sample_islands/IslandFromPlugin.tsx"], - }, - }; -} diff --git a/tests/fixture_plugin/utils/sample_islands/IslandFromPlugin.tsx b/tests/fixture_plugin/utils/sample_islands/IslandFromPlugin.tsx deleted file mode 100644 index b0dfb281cb2..00000000000 --- a/tests/fixture_plugin/utils/sample_islands/IslandFromPlugin.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { IS_BROWSER } from "../../../../src/runtime/utils.ts"; - -export default function IslandFromPlugin() { - const id = IS_BROWSER ? "csr_alt_folder" : "ssr_alt_folder"; - return ( -
-

{id}

-
- ); -} diff --git a/tests/fixture_plugin/utils/sample_routes/AppBuilder.tsx b/tests/fixture_plugin/utils/sample_routes/AppBuilder.tsx deleted file mode 100644 index 8164cd890d8..00000000000 --- a/tests/fixture_plugin/utils/sample_routes/AppBuilder.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { defineApp, PageProps } from "$fresh/server.ts"; -import { Head } from "../../../../runtime.ts"; -import { Options } from "../route-plugin.ts"; - -export function AppBuilder(options: Options) { - return options.async - ? defineApp((_req, ctx) => { - return ( - <> - - {options.title} - -
- foo - -
- - ); - }) - : ({ Component }: PageProps) => { - return ( - <> - - {options.title} - -
- -
- - ); - }; -} diff --git a/tests/fixture_plugin/utils/sample_routes/PluginRouteWithIsland.tsx b/tests/fixture_plugin/utils/sample_routes/PluginRouteWithIsland.tsx deleted file mode 100644 index 3cd1e3d3854..00000000000 --- a/tests/fixture_plugin/utils/sample_routes/PluginRouteWithIsland.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import Island from "../../islands/Island.tsx"; -import IslandFromPlugin from "../sample_islands/IslandFromPlugin.tsx"; - -export default function IslandPluginComponent() { - return ( -
- - -
- ); -} diff --git a/tests/fixture_plugin/utils/sample_routes/_middleware.ts b/tests/fixture_plugin/utils/sample_routes/_middleware.ts deleted file mode 100644 index cd3d914c76b..00000000000 --- a/tests/fixture_plugin/utils/sample_routes/_middleware.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { FreshContext } from "$fresh/server.ts"; -import { PluginMiddlewareState } from "../../utils/route-plugin.ts"; - -export async function handler( - _req: Request, - ctx: FreshContext, -) { - ctx.state.test = "look, i'm set from a plugin!"; - const resp = await ctx.next(); - return resp; -} diff --git a/tests/fixture_plugin/utils/sample_routes/async-route.tsx b/tests/fixture_plugin/utils/sample_routes/async-route.tsx deleted file mode 100644 index ebf0fc5abba..00000000000 --- a/tests/fixture_plugin/utils/sample_routes/async-route.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { defineRoute } from "$fresh/src/server/defines.ts"; - -export default defineRoute((_req, _ctx) => { - return ( -
- this is an async route! -
- ); -}); diff --git a/tests/fixture_plugin/utils/sample_routes/simple-route.tsx b/tests/fixture_plugin/utils/sample_routes/simple-route.tsx deleted file mode 100644 index 8c8de652314..00000000000 --- a/tests/fixture_plugin/utils/sample_routes/simple-route.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export function SimpleRoute() { - return
Hello
; -} diff --git a/tests/fixture_plugin/utils/second-middleware-plugin.ts b/tests/fixture_plugin/utils/second-middleware-plugin.ts deleted file mode 100644 index abee9aa9ae1..00000000000 --- a/tests/fixture_plugin/utils/second-middleware-plugin.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { FreshContext, Plugin } from "$fresh/server.ts"; -import { PluginMiddlewareState } from "./route-plugin.ts"; - -export default function secondMiddlewarePlugin(): Plugin< - PluginMiddlewareState -> { - return { - name: "secondMiddlewarePlugin", - middlewares: [{ - middleware: { - handler: async ( - _req: Request, - ctx: FreshContext, - ) => { - return await ctx.next(); - }, - }, - path: "/", // this is the root route - }, { - middleware: { - handler: async ( - _req: Request, - ctx: FreshContext, - ) => { - ctx.state.num = ctx.state.num === undefined ? 1 : ctx.state.num + 1; - return await ctx.next(); - }, - }, - path: "lots-of-middleware/", - }, { - middleware: { - handler: async ( - _req: Request, - ctx: FreshContext, - ) => { - return await ctx.next(); - }, - }, - path: "", // this also goes to the root route, as of 1.4 - }], - }; -} diff --git a/tests/fixture_plugin_error/deno.json b/tests/fixture_plugin_error/deno.json deleted file mode 100644 index d4f6a76708f..00000000000 --- a/tests/fixture_plugin_error/deno.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "lock": false, - "imports": { - "$fresh/": "../../", - "preact": "https://esm.sh/preact@10.22.0", - "preact/": "https://esm.sh/preact@10.22.0/", - "preact-render-to-string": "https://esm.sh/*preact-render-to-string@6.2.1", - "@preact/signals": "https://esm.sh/*@preact/signals@1.1.3", - "@preact/signals-core": "https://esm.sh/@preact/signals-core@1.2.3", - "@preact/signals-core@1.2.3": "https://esm.sh/@preact/signals-core@1.2.3", - "@preact/signals-core@1.3.0": "https://esm.sh/@preact/signals-core@1.3.0" - }, - "compilerOptions": { - "jsx": "react-jsx", - "jsxImportSource": "preact" - } -} diff --git a/tests/fixture_plugin_error/dev.ts b/tests/fixture_plugin_error/dev.ts deleted file mode 100755 index 2d85d6c183c..00000000000 --- a/tests/fixture_plugin_error/dev.ts +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env -S deno run -A --watch=static/,routes/ - -import dev from "$fresh/dev.ts"; - -await dev(import.meta.url, "./main.ts"); diff --git a/tests/fixture_plugin_error/fresh.gen.ts b/tests/fixture_plugin_error/fresh.gen.ts deleted file mode 100644 index da62588d9a4..00000000000 --- a/tests/fixture_plugin_error/fresh.gen.ts +++ /dev/null @@ -1,19 +0,0 @@ -// DO NOT EDIT. This file is generated by Fresh. -// This file SHOULD be checked into source version control. -// This file is automatically updated during development when running `dev.ts`. - -import * as $index from "./routes/index.tsx"; -import * as $Island from "./islands/Island.tsx"; -import { type Manifest } from "$fresh/server.ts"; - -const manifest = { - routes: { - "./routes/index.tsx": $index, - }, - islands: { - "./islands/Island.tsx": $Island, - }, - baseUrl: import.meta.url, -} satisfies Manifest; - -export default manifest; diff --git a/tests/fixture_plugin_error/islands/Island.tsx b/tests/fixture_plugin_error/islands/Island.tsx deleted file mode 100644 index ced6810fcd2..00000000000 --- a/tests/fixture_plugin_error/islands/Island.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { useEffect } from "preact/hooks"; -import { useSignal } from "@preact/signals"; - -export default function Island() { - const sig = useSignal(false); - const count = useSignal(0); - useEffect(() => { - sig.value = true; - }, []); - - return ( -
-

{count}

- -
- ); -} diff --git a/tests/fixture_plugin_error/main.ts b/tests/fixture_plugin_error/main.ts deleted file mode 100644 index b1b0d543da9..00000000000 --- a/tests/fixture_plugin_error/main.ts +++ /dev/null @@ -1,27 +0,0 @@ -/// -/// -/// -/// -/// - -import { start } from "$fresh/server.ts"; -import manifest from "./fresh.gen.ts"; - -function throwErr() { - throw new Error("Error thrown"); -} - -await start(manifest, { - plugins: [ - { - name: "thrower", - entrypoints: { - main: `data:application/javascript,export default ${throwErr}`, - }, - render(ctx) { - ctx.render(); - return { scripts: [{ entrypoint: "main", state: {} }] }; - }, - }, - ], -}); diff --git a/tests/fixture_plugin_error/routes/index.tsx b/tests/fixture_plugin_error/routes/index.tsx deleted file mode 100644 index e0673948d1b..00000000000 --- a/tests/fixture_plugin_error/routes/index.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import Island from "../islands/Island.tsx"; - -export default function Home() { - return ; -} diff --git a/tests/fixture_plugin_html/deno.json b/tests/fixture_plugin_html/deno.json deleted file mode 100644 index b211f1f4233..00000000000 --- a/tests/fixture_plugin_html/deno.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "lock": false, - "imports": { - "$fresh/": "../../", - "preact": "https://esm.sh/preact@10.22.0", - "preact/": "https://esm.sh/preact@10.22.0/" - }, - "compilerOptions": { - "jsx": "react-jsx", - "jsxImportSource": "preact" - } -} diff --git a/tests/fixture_plugin_html/dev.ts b/tests/fixture_plugin_html/dev.ts deleted file mode 100755 index 2d85d6c183c..00000000000 --- a/tests/fixture_plugin_html/dev.ts +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env -S deno run -A --watch=static/,routes/ - -import dev from "$fresh/dev.ts"; - -await dev(import.meta.url, "./main.ts"); diff --git a/tests/fixture_plugin_html/fresh.config.ts b/tests/fixture_plugin_html/fresh.config.ts deleted file mode 100644 index 7bc5833402a..00000000000 --- a/tests/fixture_plugin_html/fresh.config.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { defineConfig } from "$fresh/server.ts"; - -export default defineConfig({ - plugins: [ - { - name: "html inject", - async renderAsync(ctx) { - await ctx.renderAsync(); - return { - htmlText: "

it works

", - }; - }, - }, - ], -}); diff --git a/tests/fixture_plugin_html/fresh.gen.ts b/tests/fixture_plugin_html/fresh.gen.ts deleted file mode 100644 index b5bfec72c19..00000000000 --- a/tests/fixture_plugin_html/fresh.gen.ts +++ /dev/null @@ -1,17 +0,0 @@ -// DO NOT EDIT. This file is generated by Fresh. -// This file SHOULD be checked into source version control. -// This file is automatically updated during development when running `dev.ts`. - -import * as $index from "./routes/index.tsx"; - -import { type Manifest } from "$fresh/server.ts"; - -const manifest = { - routes: { - "./routes/index.tsx": $index, - }, - islands: {}, - baseUrl: import.meta.url, -} satisfies Manifest; - -export default manifest; diff --git a/tests/fixture_plugin_html/main.ts b/tests/fixture_plugin_html/main.ts deleted file mode 100644 index fc9359215e3..00000000000 --- a/tests/fixture_plugin_html/main.ts +++ /dev/null @@ -1,11 +0,0 @@ -/// -/// -/// -/// -/// - -import { start } from "$fresh/server.ts"; -import manifest from "./fresh.gen.ts"; -import config from "./fresh.config.ts"; - -await start(manifest, config); diff --git a/tests/fixture_plugin_html/routes/index.tsx b/tests/fixture_plugin_html/routes/index.tsx deleted file mode 100644 index 33b1bcd8b3b..00000000000 --- a/tests/fixture_plugin_html/routes/index.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export default function Foo() { - return

it does not work

; -} diff --git a/tests/fixture_plugin_lifecycle/deno.json b/tests/fixture_plugin_lifecycle/deno.json deleted file mode 100644 index 9d48eadf1c3..00000000000 --- a/tests/fixture_plugin_lifecycle/deno.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "lock": false, - "imports": { - "$fresh/": "../../", - "preact": "https://esm.sh/preact@10.22.0", - "preact/": "https://esm.sh/preact@10.22.0/", - "preact-render-to-string": "https://esm.sh/*preact-render-to-string@6.2.1" - }, - "compilerOptions": { - "jsx": "react-jsx", - "jsxImportSource": "preact" - } -} diff --git a/tests/fixture_plugin_lifecycle/dev.ts b/tests/fixture_plugin_lifecycle/dev.ts deleted file mode 100755 index 1fe3e340282..00000000000 --- a/tests/fixture_plugin_lifecycle/dev.ts +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env -S deno run -A --watch=static/,routes/ - -import dev from "$fresh/dev.ts"; -import config from "./fresh.config.ts"; - -await dev(import.meta.url, "./main.ts", config); diff --git a/tests/fixture_plugin_lifecycle/fresh.config.ts b/tests/fixture_plugin_lifecycle/fresh.config.ts deleted file mode 100644 index d9736f2df18..00000000000 --- a/tests/fixture_plugin_lifecycle/fresh.config.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { defineConfig } from "$fresh/src/server/defines.ts"; -import { relative } from "../deps.ts"; - -export default defineConfig({ - plugins: [ - { - name: "a", - configResolved() { - console.log("Plugin a: configResolved"); - }, - buildEnd() { - console.log("Plugin a: buildEnd"); - }, - buildStart() { - console.log("Plugin a: buildStart"); - }, - }, - { - name: "b", - configResolved() { - console.log("Plugin b: configResolved"); - }, - buildEnd() { - console.log("Plugin b: buildEnd"); - }, - buildStart() { - console.log("Plugin b: buildStart"); - }, - }, - { - name: "c", - configResolved() { - console.log("Plugin c: configResolved"); - }, - buildStart(config) { - const outDir = relative(Deno.cwd(), config.build.outDir); - console.log(`Plugin c: ${outDir}`); - }, - }, - ], -}); diff --git a/tests/fixture_plugin_lifecycle/fresh.gen.ts b/tests/fixture_plugin_lifecycle/fresh.gen.ts deleted file mode 100644 index 5f36a456139..00000000000 --- a/tests/fixture_plugin_lifecycle/fresh.gen.ts +++ /dev/null @@ -1,17 +0,0 @@ -// DO NOT EDIT. This file is generated by Fresh. -// This file SHOULD be checked into source version control. -// This file is automatically updated during development when running `dev.ts`. - -import * as $index from "./routes/index.tsx"; - -import type { Manifest } from "$fresh/server.ts"; - -const manifest = { - routes: { - "./routes/index.tsx": $index, - }, - islands: {}, - baseUrl: import.meta.url, -} satisfies Manifest; - -export default manifest; diff --git a/tests/fixture_plugin_lifecycle/main.ts b/tests/fixture_plugin_lifecycle/main.ts deleted file mode 100644 index fc9359215e3..00000000000 --- a/tests/fixture_plugin_lifecycle/main.ts +++ /dev/null @@ -1,11 +0,0 @@ -/// -/// -/// -/// -/// - -import { start } from "$fresh/server.ts"; -import manifest from "./fresh.gen.ts"; -import config from "./fresh.config.ts"; - -await start(manifest, config); diff --git a/tests/fixture_plugin_lifecycle/routes/index.tsx b/tests/fixture_plugin_lifecycle/routes/index.tsx deleted file mode 100644 index b42a1f58d01..00000000000 --- a/tests/fixture_plugin_lifecycle/routes/index.tsx +++ /dev/null @@ -1,7 +0,0 @@ -export default function Home() { - return ( -
-

Hello World

-
- ); -} diff --git a/tests/fixture_plugin_middleware/deno.json b/tests/fixture_plugin_middleware/deno.json deleted file mode 100644 index b211f1f4233..00000000000 --- a/tests/fixture_plugin_middleware/deno.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "lock": false, - "imports": { - "$fresh/": "../../", - "preact": "https://esm.sh/preact@10.22.0", - "preact/": "https://esm.sh/preact@10.22.0/" - }, - "compilerOptions": { - "jsx": "react-jsx", - "jsxImportSource": "preact" - } -} diff --git a/tests/fixture_plugin_middleware/dev.ts b/tests/fixture_plugin_middleware/dev.ts deleted file mode 100755 index 2d85d6c183c..00000000000 --- a/tests/fixture_plugin_middleware/dev.ts +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env -S deno run -A --watch=static/,routes/ - -import dev from "$fresh/dev.ts"; - -await dev(import.meta.url, "./main.ts"); diff --git a/tests/fixture_plugin_middleware/fresh.config.ts b/tests/fixture_plugin_middleware/fresh.config.ts deleted file mode 100644 index 0af825a9b65..00000000000 --- a/tests/fixture_plugin_middleware/fresh.config.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { defineConfig } from "$fresh/server.ts"; -import { middlewarePlugin } from "./plugins/middleware.ts"; - -export default defineConfig({ - plugins: [ - middlewarePlugin(), - ], -}); diff --git a/tests/fixture_plugin_middleware/fresh.gen.ts b/tests/fixture_plugin_middleware/fresh.gen.ts deleted file mode 100644 index a1be306e4d9..00000000000 --- a/tests/fixture_plugin_middleware/fresh.gen.ts +++ /dev/null @@ -1,19 +0,0 @@ -// DO NOT EDIT. This file is generated by Fresh. -// This file SHOULD be checked into source version control. -// This file is automatically updated during development when running `dev.ts`. - -import * as $_middleware from "./routes/_middleware.ts"; -import * as $index from "./routes/index.tsx"; - -import { type Manifest } from "$fresh/server.ts"; - -const manifest = { - routes: { - "./routes/_middleware.ts": $_middleware, - "./routes/index.tsx": $index, - }, - islands: {}, - baseUrl: import.meta.url, -} satisfies Manifest; - -export default manifest; diff --git a/tests/fixture_plugin_middleware/main.ts b/tests/fixture_plugin_middleware/main.ts deleted file mode 100644 index fc9359215e3..00000000000 --- a/tests/fixture_plugin_middleware/main.ts +++ /dev/null @@ -1,11 +0,0 @@ -/// -/// -/// -/// -/// - -import { start } from "$fresh/server.ts"; -import manifest from "./fresh.gen.ts"; -import config from "./fresh.config.ts"; - -await start(manifest, config); diff --git a/tests/fixture_plugin_middleware/plugins/middleware.ts b/tests/fixture_plugin_middleware/plugins/middleware.ts deleted file mode 100644 index a31b54d8be2..00000000000 --- a/tests/fixture_plugin_middleware/plugins/middleware.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Plugin } from "$fresh/server.ts"; - -export function middlewarePlugin(): Plugin { - return { - name: "mw-plugin", - middlewares: [ - { - path: "/", - middleware: { - handler: (req, ctx) => { - console.log("hey", req.url); - return ctx.next(); - }, - }, - }, - ], - }; -} diff --git a/tests/fixture_plugin_middleware/routes/_middleware.ts b/tests/fixture_plugin_middleware/routes/_middleware.ts deleted file mode 100644 index ccc8b2b8f6d..00000000000 --- a/tests/fixture_plugin_middleware/routes/_middleware.ts +++ /dev/null @@ -1,8 +0,0 @@ -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; -} diff --git a/tests/fixture_plugin_middleware/routes/index.tsx b/tests/fixture_plugin_middleware/routes/index.tsx deleted file mode 100644 index b365cb64dba..00000000000 --- a/tests/fixture_plugin_middleware/routes/index.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export default function Page() { - return

foo

; -} diff --git a/tests/fixture_plugin_resolved_dev/deno.json b/tests/fixture_plugin_resolved_dev/deno.json deleted file mode 100644 index 9d48eadf1c3..00000000000 --- a/tests/fixture_plugin_resolved_dev/deno.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "lock": false, - "imports": { - "$fresh/": "../../", - "preact": "https://esm.sh/preact@10.22.0", - "preact/": "https://esm.sh/preact@10.22.0/", - "preact-render-to-string": "https://esm.sh/*preact-render-to-string@6.2.1" - }, - "compilerOptions": { - "jsx": "react-jsx", - "jsxImportSource": "preact" - } -} diff --git a/tests/fixture_plugin_resolved_dev/dev.ts b/tests/fixture_plugin_resolved_dev/dev.ts deleted file mode 100755 index 1fe3e340282..00000000000 --- a/tests/fixture_plugin_resolved_dev/dev.ts +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env -S deno run -A --watch=static/,routes/ - -import dev from "$fresh/dev.ts"; -import config from "./fresh.config.ts"; - -await dev(import.meta.url, "./main.ts", config); diff --git a/tests/fixture_plugin_resolved_dev/fresh.config.ts b/tests/fixture_plugin_resolved_dev/fresh.config.ts deleted file mode 100644 index 7807a94b17f..00000000000 --- a/tests/fixture_plugin_resolved_dev/fresh.config.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { defineConfig } from "$fresh/src/server/defines.ts"; - -const delay = (ms: number) => new Promise((r) => setTimeout(r, ms)); - -let called = false; -export default defineConfig({ - plugins: [ - { - name: "a", - async configResolved() { - await delay(1); - called = true; - }, - middlewares: [ - { - path: "/", - middleware: { - handler: async (_req, ctx) => { - const res = await ctx.next(); - res.headers.append("X-Plugin-A", String(called)); - return res; - }, - }, - }, - ], - }, - ], -}); diff --git a/tests/fixture_plugin_resolved_dev/fresh.gen.ts b/tests/fixture_plugin_resolved_dev/fresh.gen.ts deleted file mode 100644 index b5bfec72c19..00000000000 --- a/tests/fixture_plugin_resolved_dev/fresh.gen.ts +++ /dev/null @@ -1,17 +0,0 @@ -// DO NOT EDIT. This file is generated by Fresh. -// This file SHOULD be checked into source version control. -// This file is automatically updated during development when running `dev.ts`. - -import * as $index from "./routes/index.tsx"; - -import { type Manifest } from "$fresh/server.ts"; - -const manifest = { - routes: { - "./routes/index.tsx": $index, - }, - islands: {}, - baseUrl: import.meta.url, -} satisfies Manifest; - -export default manifest; diff --git a/tests/fixture_plugin_resolved_dev/main.ts b/tests/fixture_plugin_resolved_dev/main.ts deleted file mode 100644 index fc9359215e3..00000000000 --- a/tests/fixture_plugin_resolved_dev/main.ts +++ /dev/null @@ -1,11 +0,0 @@ -/// -/// -/// -/// -/// - -import { start } from "$fresh/server.ts"; -import manifest from "./fresh.gen.ts"; -import config from "./fresh.config.ts"; - -await start(manifest, config); diff --git a/tests/fixture_plugin_resolved_dev/routes/index.tsx b/tests/fixture_plugin_resolved_dev/routes/index.tsx deleted file mode 100644 index b42a1f58d01..00000000000 --- a/tests/fixture_plugin_resolved_dev/routes/index.tsx +++ /dev/null @@ -1,7 +0,0 @@ -export default function Home() { - return ( -
-

Hello World

-
- ); -} diff --git a/tests/fixture_precompile/invalid/deno.json b/tests/fixture_precompile/invalid/deno.json new file mode 100644 index 00000000000..9ca8685e176 --- /dev/null +++ b/tests/fixture_precompile/invalid/deno.json @@ -0,0 +1,21 @@ +{ + "lock": false, + "compilerOptions": { + "jsx": "precompile", + "jsxImportSource": "preact" + }, + "imports": { + "@luca/esbuild-deno-loader": "jsr:@luca/esbuild-deno-loader@^0.10.3", + "@std/crypto": "jsr:@std/crypto@^0.224.0", + "@std/datetime": "jsr:@std/datetime@^0.224.0", + "@std/encoding": "jsr:@std/encoding@^0.224.0", + "@std/fmt": "jsr:@std/fmt@^0.224.0", + "@std/fs": "jsr:@std/fs@^0.224.0", + "@std/html": "jsr:@std/html@^0.224.0", + "@std/jsonc": "jsr:@std/jsonc@^0.224.0", + "@std/path": "jsr:@std/path@^0.224.0", + "@std/semver": "jsr:@std/semver@^0.224.0", + "preact": "npm:preact@^10.20.2", + "preact-render-to-string": "npm:preact-render-to-string@^6.4.2" + } +} diff --git a/tests/fixture_precompile/invalid/dev.ts b/tests/fixture_precompile/invalid/dev.ts new file mode 100644 index 00000000000..85f6aab81cc --- /dev/null +++ b/tests/fixture_precompile/invalid/dev.ts @@ -0,0 +1,8 @@ +import { Builder } from "../../../src/dev/mod.ts"; +import { app } from "./main.tsx"; + +const builder = new Builder(); + +await builder.listen(app, { + port: 4001, +}); diff --git a/tests/fixture_precompile/invalid/main.tsx b/tests/fixture_precompile/invalid/main.tsx new file mode 100644 index 00000000000..e42795f8855 --- /dev/null +++ b/tests/fixture_precompile/invalid/main.tsx @@ -0,0 +1,6 @@ +import { App } from "../../../src/app.ts"; + +export const app = new App().get( + "/", + () => new Response("hello"), +); diff --git a/tests/fixture_precompile/valid/deno.json b/tests/fixture_precompile/valid/deno.json new file mode 100644 index 00000000000..f496373b8c0 --- /dev/null +++ b/tests/fixture_precompile/valid/deno.json @@ -0,0 +1,16 @@ +{ + "lock": false, + "compilerOptions": { + "jsx": "precompile", + "jsxImportSource": "preact", + "jsxPrecompileSkipElements": ["a", "img", "source", "body", "html", "head"] + }, + "imports": { + "@std/encoding": "jsr:@std/encoding@^0.224.0", + "@std/fmt": "jsr:@std/fmt@^0.224.0", + "@std/html": "jsr:@std/html@^0.224.0", + "@std/path": "jsr:@std/path@^0.224.0", + "preact": "npm:preact@^10.20.2", + "preact-render-to-string": "npm:preact-render-to-string@^6.4.2" + } +} diff --git a/tests/fixture_precompile/valid/main.tsx b/tests/fixture_precompile/valid/main.tsx new file mode 100644 index 00000000000..3b31e7e4a64 --- /dev/null +++ b/tests/fixture_precompile/valid/main.tsx @@ -0,0 +1,30 @@ +import { App } from "../../../src/app.ts"; + +const app = new App({ staticDir: "./static" }).get( + "/", + (ctx) => + ctx.render( + + + + foo + + +
+ +

false

+
+ Home + + + + +
+ + , + ), +); + +const handler = await app.handler(); +const res = await handler(new Request("http://localhost/")); +console.log(await res.text()); diff --git a/tests/fixture_render/deno.json b/tests/fixture_render/deno.json deleted file mode 100644 index bbf4ca6e5db..00000000000 --- a/tests/fixture_render/deno.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "lock": false, - "tasks": { - "start": "deno run -A --watch=static/,routes/ dev.ts" - }, - "imports": { - "$fresh/": "../../", - "preact": "https://esm.sh/preact@10.22.0", - "preact/": "https://esm.sh/preact@10.22.0/", - "preact-render-to-string": "https://esm.sh/*preact-render-to-string@6.2.1" - }, - "compilerOptions": { - "jsx": "react-jsx", - "jsxImportSource": "preact" - } -} diff --git a/tests/fixture_render/dev.ts b/tests/fixture_render/dev.ts deleted file mode 100755 index 2d85d6c183c..00000000000 --- a/tests/fixture_render/dev.ts +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env -S deno run -A --watch=static/,routes/ - -import dev from "$fresh/dev.ts"; - -await dev(import.meta.url, "./main.ts"); diff --git a/tests/fixture_render/fresh.gen.ts b/tests/fixture_render/fresh.gen.ts deleted file mode 100644 index 7c17cec94b1..00000000000 --- a/tests/fixture_render/fresh.gen.ts +++ /dev/null @@ -1,27 +0,0 @@ -// DO NOT EDIT. This file is generated by Fresh. -// This file SHOULD be checked into source version control. -// This file is automatically updated during development when running `dev.ts`. - -import * as $cookiePasser from "./routes/cookiePasser.tsx"; -import * as $head_style from "./routes/head_style.tsx"; -import * as $header_arr from "./routes/header_arr.tsx"; -import * as $header_instance from "./routes/header_instance.tsx"; -import * as $header_obj from "./routes/header_obj.tsx"; -import * as $index from "./routes/index.tsx"; - -import { type Manifest } from "$fresh/server.ts"; - -const manifest = { - routes: { - "./routes/cookiePasser.tsx": $cookiePasser, - "./routes/head_style.tsx": $head_style, - "./routes/header_arr.tsx": $header_arr, - "./routes/header_instance.tsx": $header_instance, - "./routes/header_obj.tsx": $header_obj, - "./routes/index.tsx": $index, - }, - islands: {}, - baseUrl: import.meta.url, -} satisfies Manifest; - -export default manifest; diff --git a/tests/fixture_render/main.ts b/tests/fixture_render/main.ts deleted file mode 100644 index dedce9cbb04..00000000000 --- a/tests/fixture_render/main.ts +++ /dev/null @@ -1,10 +0,0 @@ -/// -/// -/// -/// -/// - -import { start } from "$fresh/server.ts"; -import manifest from "./fresh.gen.ts"; - -await start(manifest); diff --git a/tests/fixture_render/routes/cookiePasser.tsx b/tests/fixture_render/routes/cookiePasser.tsx deleted file mode 100644 index 6936283e247..00000000000 --- a/tests/fixture_render/routes/cookiePasser.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { FreshContext, Handlers } from "$fresh/server.ts"; - -export const handler: Handlers = { - async GET(_req: Request, ctx: FreshContext) { - const headers = new Headers(); - headers.append("Set-Cookie", "foo=bar"); - headers.append("Set-Cookie", "baz=1234"); - return await ctx.render({}, { headers }); - }, -}; - -export default function Home() { - return ( -
- hello -
- ); -} diff --git a/tests/fixture_render/routes/head_style.tsx b/tests/fixture_render/routes/head_style.tsx deleted file mode 100644 index fce604d6148..00000000000 --- a/tests/fixture_render/routes/head_style.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { RouteConfig } from "$fresh/src/server/types.ts"; - -export const config: RouteConfig = { - skipAppWrapper: true, - skipInheritedLayouts: true, -}; - -export default function App() { - return ( - - - - - - hello - - - ); -} diff --git a/tests/fixture_render/routes/header_arr.tsx b/tests/fixture_render/routes/header_arr.tsx deleted file mode 100644 index eef2f6d33bc..00000000000 --- a/tests/fixture_render/routes/header_arr.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import type { Handlers } from "$fresh/server.ts"; - -export const handler: Handlers = { - GET(_, ctx) { - const headers = [["x-foo", "Hello world!"]] as [string, string][]; - return ctx.render(undefined, { headers }); - }, -}; - -export default function Home() { - return ( -
- Should have X-Foo header set. -
- ); -} diff --git a/tests/fixture_render/routes/header_instance.tsx b/tests/fixture_render/routes/header_instance.tsx deleted file mode 100644 index 54c1f96f5c1..00000000000 --- a/tests/fixture_render/routes/header_instance.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import type { Handlers } from "$fresh/server.ts"; - -export const handler: Handlers = { - GET(_, ctx) { - const headers = new Headers(); - headers.set("x-foo", "Hello world!"); - return ctx.render(undefined, { headers }); - }, -}; - -export default function Home() { - return ( -
- Should have X-Foo header set. -
- ); -} diff --git a/tests/fixture_render/routes/header_obj.tsx b/tests/fixture_render/routes/header_obj.tsx deleted file mode 100644 index 22fc22ba9e1..00000000000 --- a/tests/fixture_render/routes/header_obj.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import type { Handlers } from "$fresh/server.ts"; - -export const handler: Handlers = { - GET(_, ctx) { - const headers = { - "x-foo": "Hello world!", - }; - return ctx.render(undefined, { headers }); - }, -}; - -export default function Home() { - return ( -
- Should have X-Foo header set. -
- ); -} diff --git a/tests/fixture_render/routes/index.tsx b/tests/fixture_render/routes/index.tsx deleted file mode 100644 index d50c5fee82e..00000000000 --- a/tests/fixture_render/routes/index.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { basename, dirname, extname, fromFileUrl } from "../../deps.ts"; - -const __dirname = dirname(fromFileUrl(import.meta.url)); - -const links: string[] = []; -for (const file of Deno.readDirSync(__dirname)) { - if (file.name.startsWith("index")) continue; - const name = basename(file.name, extname(file.name)); - links.push(name); -} - -export default function Home() { - return ( -
-

Tests

-
    - {links.sort().map((link) => { - return ( -
  • - {link} -
  • - ); - })} -
-
- ); -} diff --git a/tests/fixture_render_error/deno.json b/tests/fixture_render_error/deno.json deleted file mode 100644 index b211f1f4233..00000000000 --- a/tests/fixture_render_error/deno.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "lock": false, - "imports": { - "$fresh/": "../../", - "preact": "https://esm.sh/preact@10.22.0", - "preact/": "https://esm.sh/preact@10.22.0/" - }, - "compilerOptions": { - "jsx": "react-jsx", - "jsxImportSource": "preact" - } -} diff --git a/tests/fixture_render_error/dev.ts b/tests/fixture_render_error/dev.ts deleted file mode 100755 index 2d85d6c183c..00000000000 --- a/tests/fixture_render_error/dev.ts +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env -S deno run -A --watch=static/,routes/ - -import dev from "$fresh/dev.ts"; - -await dev(import.meta.url, "./main.ts"); diff --git a/tests/fixture_render_error/fresh.gen.ts b/tests/fixture_render_error/fresh.gen.ts deleted file mode 100644 index b5bfec72c19..00000000000 --- a/tests/fixture_render_error/fresh.gen.ts +++ /dev/null @@ -1,17 +0,0 @@ -// DO NOT EDIT. This file is generated by Fresh. -// This file SHOULD be checked into source version control. -// This file is automatically updated during development when running `dev.ts`. - -import * as $index from "./routes/index.tsx"; - -import { type Manifest } from "$fresh/server.ts"; - -const manifest = { - routes: { - "./routes/index.tsx": $index, - }, - islands: {}, - baseUrl: import.meta.url, -} satisfies Manifest; - -export default manifest; diff --git a/tests/fixture_render_error/main.ts b/tests/fixture_render_error/main.ts deleted file mode 100644 index dedce9cbb04..00000000000 --- a/tests/fixture_render_error/main.ts +++ /dev/null @@ -1,10 +0,0 @@ -/// -/// -/// -/// -/// - -import { start } from "$fresh/server.ts"; -import manifest from "./fresh.gen.ts"; - -await start(manifest); diff --git a/tests/fixture_render_error/routes/index.tsx b/tests/fixture_render_error/routes/index.tsx deleted file mode 100644 index 4ddf89e7d6e..00000000000 --- a/tests/fixture_render_error/routes/index.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export default function Home() { - return

it should error {{ foo: 123 }}

; -} diff --git a/tests/fixture_route_analysis/deno.json b/tests/fixture_route_analysis/deno.json deleted file mode 100644 index 4f5c09068a4..00000000000 --- a/tests/fixture_route_analysis/deno.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "lock": false, - "tasks": { - "start": "deno run -A --watch=static/,routes/ dev.ts", - "update": "deno run -A -r https://fresh.deno.dev/update ." - }, - "imports": { - "$fresh/": "../../", - "preact": "https://esm.sh/preact@10.22.0", - "preact/": "https://esm.sh/preact@10.22.0/", - "@preact/signals": "https://esm.sh/*@preact/signals@1.1.3", - "@preact/signals-core": "https://esm.sh/*@preact/signals-core@1.2.3", - "$std/": "https://deno.land/std@0.211.0/" - }, - "compilerOptions": { - "jsx": "react-jsx", - "jsxImportSource": "preact" - } -} diff --git a/tests/fixture_route_analysis/dev.ts b/tests/fixture_route_analysis/dev.ts deleted file mode 100755 index 2d85d6c183c..00000000000 --- a/tests/fixture_route_analysis/dev.ts +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env -S deno run -A --watch=static/,routes/ - -import dev from "$fresh/dev.ts"; - -await dev(import.meta.url, "./main.ts"); diff --git a/tests/fixture_route_analysis/fresh.gen.ts b/tests/fixture_route_analysis/fresh.gen.ts deleted file mode 100644 index 7d1ff0f4098..00000000000 --- a/tests/fixture_route_analysis/fresh.gen.ts +++ /dev/null @@ -1,17 +0,0 @@ -// DO NOT EDIT. This file is generated by Fresh. -// This file SHOULD be checked into source version control. -// This file is automatically updated during development when running `dev.ts`. - -import * as $0 from "./routes/simple_name_conflict/users.ts"; -import * as $1 from "./routes/simple_name_conflict/users.tsx"; - -const manifest = { - routes: { - "./routes/simple_name_conflict/users.ts": $0, - "./routes/simple_name_conflict/users.tsx": $1, - }, - islands: {}, - baseUrl: import.meta.url, -}; - -export default manifest; diff --git a/tests/fixture_route_analysis/main.ts b/tests/fixture_route_analysis/main.ts deleted file mode 100644 index 8ef5d924c9b..00000000000 --- a/tests/fixture_route_analysis/main.ts +++ /dev/null @@ -1,12 +0,0 @@ -/// -/// -/// -/// -/// - -import "$std/dotenv/load.ts"; - -import { start } from "$fresh/server.ts"; -import manifest from "./fresh.gen.ts"; - -await start(manifest); diff --git a/tests/fixture_route_analysis/routes/simple_name_conflict/users.ts b/tests/fixture_route_analysis/routes/simple_name_conflict/users.ts deleted file mode 100644 index 90d9e8983fb..00000000000 --- a/tests/fixture_route_analysis/routes/simple_name_conflict/users.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Handlers } from "$fresh/server.ts"; - -export const handler: Handlers = { - GET(_req, _ctx) { - return new Response("hello"); - }, -}; diff --git a/tests/fixture_route_analysis/routes/simple_name_conflict/users.tsx b/tests/fixture_route_analysis/routes/simple_name_conflict/users.tsx deleted file mode 100644 index 9899158d7e7..00000000000 --- a/tests/fixture_route_analysis/routes/simple_name_conflict/users.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export default function Home() { - return
Hello
; -} diff --git a/tests/fixture_router/dev.ts b/tests/fixture_router/dev.ts deleted file mode 100644 index 2d85d6c183c..00000000000 --- a/tests/fixture_router/dev.ts +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env -S deno run -A --watch=static/,routes/ - -import dev from "$fresh/dev.ts"; - -await dev(import.meta.url, "./main.ts"); diff --git a/tests/fixture_router/fresh.gen.ts b/tests/fixture_router/fresh.gen.ts deleted file mode 100644 index bba8dabe1ec..00000000000 --- a/tests/fixture_router/fresh.gen.ts +++ /dev/null @@ -1,19 +0,0 @@ -// DO NOT EDIT. This file is generated by Fresh. -// This file SHOULD be checked into source version control. -// This file is automatically updated during development when running `dev.ts`. - -import * as $about from "./routes/about.tsx"; -import * as $index from "./routes/index.tsx"; - -import { type Manifest } from "$fresh/server.ts"; - -const manifest = { - routes: { - "./routes/about.tsx": $about, - "./routes/index.tsx": $index, - }, - islands: {}, - baseUrl: import.meta.url, -} satisfies Manifest; - -export default manifest; diff --git a/tests/fixture_router/main.ts b/tests/fixture_router/main.ts deleted file mode 100644 index 0f15e8defa4..00000000000 --- a/tests/fixture_router/main.ts +++ /dev/null @@ -1,10 +0,0 @@ -/// -/// -/// -/// -/// - -import { start } from "$fresh/server.ts"; -import manifest from "./fresh.gen.ts"; - -await start(manifest); diff --git a/tests/fixture_router/routes/about.tsx b/tests/fixture_router/routes/about.tsx deleted file mode 100644 index b7f9b36995a..00000000000 --- a/tests/fixture_router/routes/about.tsx +++ /dev/null @@ -1,7 +0,0 @@ -export default function About() { - return ( -
- About -
- ); -} diff --git a/tests/fixture_router/routes/index.tsx b/tests/fixture_router/routes/index.tsx deleted file mode 100644 index e8dd3d6f5ff..00000000000 --- a/tests/fixture_router/routes/index.tsx +++ /dev/null @@ -1,7 +0,0 @@ -export default function Home() { - return ( -
- Hello -
- ); -} diff --git a/tests/fixture_router/static/foo.txt b/tests/fixture_router/static/foo.txt deleted file mode 100644 index ba0e162e1c4..00000000000 --- a/tests/fixture_router/static/foo.txt +++ /dev/null @@ -1 +0,0 @@ -bar \ No newline at end of file diff --git a/tests/fixture_router_ignore_files/deno.json b/tests/fixture_router_ignore_files/deno.json deleted file mode 100644 index ec89c30c61a..00000000000 --- a/tests/fixture_router_ignore_files/deno.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "lock": false, - "imports": { - "$fresh/": "../../", - "preact": "https://esm.sh/preact@10.22.0", - "preact/": "https://esm.sh/preact@10.22.0/", - "preact-render-to-string": "https://esm.sh/*preact-render-to-string@6.2.1", - "@preact/signals": "https://esm.sh/*@preact/signals@1.1.5", - "@preact/signals-core": "https://esm.sh/@preact/signals-core@1.3.1" - }, - "compilerOptions": { - "jsx": "react-jsx", - "jsxImportSource": "preact" - } -} diff --git a/tests/fixture_router_ignore_files/dev.ts b/tests/fixture_router_ignore_files/dev.ts deleted file mode 100755 index 1fe3e340282..00000000000 --- a/tests/fixture_router_ignore_files/dev.ts +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env -S deno run -A --watch=static/,routes/ - -import dev from "$fresh/dev.ts"; -import config from "./fresh.config.ts"; - -await dev(import.meta.url, "./main.ts", config); diff --git a/tests/fixture_router_ignore_files/fresh.config.ts b/tests/fixture_router_ignore_files/fresh.config.ts deleted file mode 100644 index 473657f55cd..00000000000 --- a/tests/fixture_router_ignore_files/fresh.config.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { FreshConfig } from "$fresh/server.ts"; - -export default { - router: { - ignoreFilePattern: /[\.|_]cy\.[t|j]s(x)?$/, - }, -} as FreshConfig; diff --git a/tests/fixture_router_ignore_files/fresh.gen.ts b/tests/fixture_router_ignore_files/fresh.gen.ts deleted file mode 100644 index b5bfec72c19..00000000000 --- a/tests/fixture_router_ignore_files/fresh.gen.ts +++ /dev/null @@ -1,17 +0,0 @@ -// DO NOT EDIT. This file is generated by Fresh. -// This file SHOULD be checked into source version control. -// This file is automatically updated during development when running `dev.ts`. - -import * as $index from "./routes/index.tsx"; - -import { type Manifest } from "$fresh/server.ts"; - -const manifest = { - routes: { - "./routes/index.tsx": $index, - }, - islands: {}, - baseUrl: import.meta.url, -} satisfies Manifest; - -export default manifest; diff --git a/tests/fixture_router_ignore_files/main.ts b/tests/fixture_router_ignore_files/main.ts deleted file mode 100644 index fc9359215e3..00000000000 --- a/tests/fixture_router_ignore_files/main.ts +++ /dev/null @@ -1,11 +0,0 @@ -/// -/// -/// -/// -/// - -import { start } from "$fresh/server.ts"; -import manifest from "./fresh.gen.ts"; -import config from "./fresh.config.ts"; - -await start(manifest, config); diff --git a/tests/fixture_router_ignore_files/routes/index.cy.ts b/tests/fixture_router_ignore_files/routes/index.cy.ts deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/tests/fixture_router_ignore_files/routes/index.tsx b/tests/fixture_router_ignore_files/routes/index.tsx deleted file mode 100644 index 7d94ac5aa2d..00000000000 --- a/tests/fixture_router_ignore_files/routes/index.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { IS_BROWSER } from "$fresh/runtime.ts"; - -export default function Home() { - return ( -
-

{IS_BROWSER ? "Viewing browser render." : "Viewing JIT render."}

-
- ); -} diff --git a/tests/fixture_server_components/deno.json b/tests/fixture_server_components/deno.json deleted file mode 100644 index 911a397bdea..00000000000 --- a/tests/fixture_server_components/deno.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "lock": false, - "tasks": { - "start": "deno run -A --watch=static/,routes/ dev.ts" - }, - "imports": { - "$fresh/": "../../", - "twind": "https://esm.sh/twind@0.16.19", - "twind/": "https://esm.sh/twind@0.16.19/", - "preact": "https://esm.sh/preact@10.22.0", - "preact/": "https://esm.sh/preact@10.22.0/", - "@preact/signals": "https://esm.sh/*@preact/signals@1.1.3", - "@preact/signals-core": "https://esm.sh/@preact/signals-core@1.2.3" - }, - "compilerOptions": { - "jsx": "react-jsx", - "jsxImportSource": "preact" - } -} diff --git a/tests/fixture_server_components/dev.ts b/tests/fixture_server_components/dev.ts deleted file mode 100755 index 2d85d6c183c..00000000000 --- a/tests/fixture_server_components/dev.ts +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env -S deno run -A --watch=static/,routes/ - -import dev from "$fresh/dev.ts"; - -await dev(import.meta.url, "./main.ts"); diff --git a/tests/fixture_server_components/fresh.gen.ts b/tests/fixture_server_components/fresh.gen.ts deleted file mode 100644 index 2386d97f9c3..00000000000 --- a/tests/fixture_server_components/fresh.gen.ts +++ /dev/null @@ -1,31 +0,0 @@ -// DO NOT EDIT. This file is generated by Fresh. -// This file SHOULD be checked into source version control. -// This file is automatically updated during development when running `dev.ts`. - -import * as $basic from "./routes/basic.tsx"; -import * as $context_id_ from "./routes/context/[id].tsx"; -import * as $fail from "./routes/fail.tsx"; -import * as $index from "./routes/index.tsx"; -import * as $island from "./routes/island.tsx"; -import * as $response from "./routes/response.tsx"; -import * as $twind from "./routes/twind.tsx"; -import * as $FooIsland from "./islands/FooIsland.tsx"; -import { type Manifest } from "$fresh/server.ts"; - -const manifest = { - routes: { - "./routes/basic.tsx": $basic, - "./routes/context/[id].tsx": $context_id_, - "./routes/fail.tsx": $fail, - "./routes/index.tsx": $index, - "./routes/island.tsx": $island, - "./routes/response.tsx": $response, - "./routes/twind.tsx": $twind, - }, - islands: { - "./islands/FooIsland.tsx": $FooIsland, - }, - baseUrl: import.meta.url, -} satisfies Manifest; - -export default manifest; diff --git a/tests/fixture_server_components/islands/FooIsland.tsx b/tests/fixture_server_components/islands/FooIsland.tsx deleted file mode 100644 index 0d2e3bd9200..00000000000 --- a/tests/fixture_server_components/islands/FooIsland.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { useSignal } from "@preact/signals"; - -export default function FooIsland() { - const sig = useSignal(0); - return ( - - ); -} diff --git a/tests/fixture_server_components/main.ts b/tests/fixture_server_components/main.ts deleted file mode 100644 index 871f5ecb939..00000000000 --- a/tests/fixture_server_components/main.ts +++ /dev/null @@ -1,11 +0,0 @@ -/// -/// -/// -/// - -import { start } from "$fresh/server.ts"; -import twind from "$fresh/plugins/twind.ts"; -import manifest from "./fresh.gen.ts"; - -import twindConfig from "./twind.config.ts"; -await start(manifest, { plugins: [twind(twindConfig)] }); diff --git a/tests/fixture_server_components/routes/basic.tsx b/tests/fixture_server_components/routes/basic.tsx deleted file mode 100644 index 08976b0d0ce..00000000000 --- a/tests/fixture_server_components/routes/basic.tsx +++ /dev/null @@ -1,4 +0,0 @@ -export default async function Home() { - await new Promise((r) => setTimeout(r, 10)); - return

it works

; -} diff --git a/tests/fixture_server_components/routes/context/[id].tsx b/tests/fixture_server_components/routes/context/[id].tsx deleted file mode 100644 index 7a4a4034167..00000000000 --- a/tests/fixture_server_components/routes/context/[id].tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { RouteContext } from "../../../../server.ts"; -import { delay, relative, SEPARATOR } from "../../../deps.ts"; - -export default async function Foo(_req: Request, context: RouteContext) { - await delay(1); - const value = JSON.stringify(context, (key, value) => { - if (key === "outDir" || key == "staticDir") { - return relative(Deno.cwd(), value).split(SEPARATOR).join("/"); - } else if (key === "entrypoints") { - return {}; - } - if (typeof value === "function") return value.constructor.name; - if (value === undefined) return ""; - return value; - }, 2); - - return new Response(value, { - status: 200, - headers: { "Content-Type": "application/json" }, - }); -} diff --git a/tests/fixture_server_components/routes/fail.tsx b/tests/fixture_server_components/routes/fail.tsx deleted file mode 100644 index cb10826d345..00000000000 --- a/tests/fixture_server_components/routes/fail.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import { RouteContext } from "$fresh/server.ts"; - -// deno-lint-ignore require-await -export default async function Home(_req: Request, ctx: RouteContext) { - return ctx.renderNotFound(); -} diff --git a/tests/fixture_server_components/routes/index.tsx b/tests/fixture_server_components/routes/index.tsx deleted file mode 100644 index 24478694fc0..00000000000 --- a/tests/fixture_server_components/routes/index.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { basename, dirname, extname, fromFileUrl } from "../../deps.ts"; - -const __dirname = dirname(fromFileUrl(import.meta.url)); - -const links: string[] = []; -for (const file of Deno.readDirSync(__dirname)) { - if (file.name.startsWith("index")) continue; - const name = basename(file.name, extname(file.name)); - links.push(name); -} - -export default function Home() { - return ( -
-

Tests

-
    - {links.map((link) => { - return ( -
  • - {link} -
  • - ); - })} -
-
- ); -} diff --git a/tests/fixture_server_components/routes/island.tsx b/tests/fixture_server_components/routes/island.tsx deleted file mode 100644 index 84be171449e..00000000000 --- a/tests/fixture_server_components/routes/island.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import FooIsland from "../islands/FooIsland.tsx"; - -export default async function Island() { - await new Promise((r) => setTimeout(r, 10)); - return ; -} diff --git a/tests/fixture_server_components/routes/response.tsx b/tests/fixture_server_components/routes/response.tsx deleted file mode 100644 index 451a25a7274..00000000000 --- a/tests/fixture_server_components/routes/response.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import { delay } from "../../deps.ts"; - -export default async function Foo() { - await delay(1); - return new Response("it works", { status: 200 }); -} diff --git a/tests/fixture_server_components/routes/twind.tsx b/tests/fixture_server_components/routes/twind.tsx deleted file mode 100644 index 67395caa965..00000000000 --- a/tests/fixture_server_components/routes/twind.tsx +++ /dev/null @@ -1,4 +0,0 @@ -export default async function Twind() { - await new Promise((r) => setTimeout(r, 10)); - return

it works

; -} diff --git a/tests/fixture_server_components/twind.config.ts b/tests/fixture_server_components/twind.config.ts deleted file mode 100644 index 85562a9be79..00000000000 --- a/tests/fixture_server_components/twind.config.ts +++ /dev/null @@ -1,17 +0,0 @@ -import * as colors from "twind/colors"; - -export default { - selfURL: import.meta.url, - theme: { - colors: { - blue: colors.blue, - black: colors.black, - gray: colors.gray, - green: colors.green, - red: colors.red, - white: colors.white, - yellow: colors.yellow, - transparent: "transparent", - }, - }, -}; diff --git a/tests/fixture_static/deno.json b/tests/fixture_static/deno.json deleted file mode 100644 index 9d48eadf1c3..00000000000 --- a/tests/fixture_static/deno.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "lock": false, - "imports": { - "$fresh/": "../../", - "preact": "https://esm.sh/preact@10.22.0", - "preact/": "https://esm.sh/preact@10.22.0/", - "preact-render-to-string": "https://esm.sh/*preact-render-to-string@6.2.1" - }, - "compilerOptions": { - "jsx": "react-jsx", - "jsxImportSource": "preact" - } -} diff --git a/tests/fixture_static/dev.ts b/tests/fixture_static/dev.ts deleted file mode 100755 index 2d85d6c183c..00000000000 --- a/tests/fixture_static/dev.ts +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env -S deno run -A --watch=static/,routes/ - -import dev from "$fresh/dev.ts"; - -await dev(import.meta.url, "./main.ts"); diff --git a/tests/fixture_static/fresh.gen.ts b/tests/fixture_static/fresh.gen.ts deleted file mode 100644 index 28188fccbbf..00000000000 --- a/tests/fixture_static/fresh.gen.ts +++ /dev/null @@ -1,21 +0,0 @@ -// DO NOT EDIT. This file is generated by Fresh. -// This file SHOULD be checked into source version control. -// This file is automatically updated during development when running `dev.ts`. - -import * as $_app from "./routes/_app.tsx"; -import * as $css from "./routes/css.tsx"; -import * as $index from "./routes/index.tsx"; - -import { type Manifest } from "$fresh/server.ts"; - -const manifest = { - routes: { - "./routes/_app.tsx": $_app, - "./routes/css.tsx": $css, - "./routes/index.tsx": $index, - }, - islands: {}, - baseUrl: import.meta.url, -} satisfies Manifest; - -export default manifest; diff --git a/tests/fixture_static/main.ts b/tests/fixture_static/main.ts deleted file mode 100644 index dedce9cbb04..00000000000 --- a/tests/fixture_static/main.ts +++ /dev/null @@ -1,10 +0,0 @@ -/// -/// -/// -/// -/// - -import { start } from "$fresh/server.ts"; -import manifest from "./fresh.gen.ts"; - -await start(manifest); diff --git a/tests/fixture_static/routes/_app.tsx b/tests/fixture_static/routes/_app.tsx deleted file mode 100644 index a4b545836c8..00000000000 --- a/tests/fixture_static/routes/_app.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { PageProps } from "$fresh/server.ts"; - -export default function App({ Component }: PageProps) { - return ( - - - - - My Fresh app - - - - - - - ); -} diff --git a/tests/fixture_static/routes/css.tsx b/tests/fixture_static/routes/css.tsx deleted file mode 100644 index fbba9a95699..00000000000 --- a/tests/fixture_static/routes/css.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export default function Page() { - return

test

; -} diff --git a/tests/fixture_static/routes/index.tsx b/tests/fixture_static/routes/index.tsx deleted file mode 100644 index d50c5fee82e..00000000000 --- a/tests/fixture_static/routes/index.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { basename, dirname, extname, fromFileUrl } from "../../deps.ts"; - -const __dirname = dirname(fromFileUrl(import.meta.url)); - -const links: string[] = []; -for (const file of Deno.readDirSync(__dirname)) { - if (file.name.startsWith("index")) continue; - const name = basename(file.name, extname(file.name)); - links.push(name); -} - -export default function Home() { - return ( -
-

Tests

-
    - {links.sort().map((link) => { - return ( -
  • - {link} -
  • - ); - })} -
-
- ); -} diff --git a/tests/fixture_static/static/style.css b/tests/fixture_static/static/style.css deleted file mode 100644 index adc68fa6a4d..00000000000 --- a/tests/fixture_static/static/style.css +++ /dev/null @@ -1,3 +0,0 @@ -h1 { - color: red; -} diff --git a/tests/fixture_tailwind/deno.json b/tests/fixture_tailwind/deno.json deleted file mode 100644 index 6d4529cc180..00000000000 --- a/tests/fixture_tailwind/deno.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "lock": false, - "imports": { - "$fresh/": "../../", - "preact": "https://esm.sh/preact@10.22.0", - "preact/": "https://esm.sh/preact@10.22.0/", - "tailwindcss": "npm:tailwindcss@3.4.1" - }, - "compilerOptions": { - "jsx": "react-jsx", - "jsxImportSource": "preact" - } -} diff --git a/tests/fixture_tailwind/dev.ts b/tests/fixture_tailwind/dev.ts deleted file mode 100755 index 1fe3e340282..00000000000 --- a/tests/fixture_tailwind/dev.ts +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env -S deno run -A --watch=static/,routes/ - -import dev from "$fresh/dev.ts"; -import config from "./fresh.config.ts"; - -await dev(import.meta.url, "./main.ts", config); diff --git a/tests/fixture_tailwind/fresh.config.ts b/tests/fixture_tailwind/fresh.config.ts deleted file mode 100644 index 6f7acca6964..00000000000 --- a/tests/fixture_tailwind/fresh.config.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { defineConfig } from "$fresh/server.ts"; -import tailwind from "$fresh/plugins/tailwind.ts"; - -export default defineConfig({ - plugins: [ - tailwind(), - ], -}); diff --git a/tests/fixture_tailwind/fresh.gen.ts b/tests/fixture_tailwind/fresh.gen.ts deleted file mode 100644 index 2e895a65907..00000000000 --- a/tests/fixture_tailwind/fresh.gen.ts +++ /dev/null @@ -1,21 +0,0 @@ -// DO NOT EDIT. This file is generated by Fresh. -// This file SHOULD be checked into source version control. -// This file is automatically updated during development when running `dev.ts`. - -import * as $_app from "./routes/_app.tsx"; -import * as $_middleware from "./routes/_middleware.ts"; -import * as $index from "./routes/index.tsx"; - -import { type Manifest } from "$fresh/server.ts"; - -const manifest = { - routes: { - "./routes/_app.tsx": $_app, - "./routes/_middleware.ts": $_middleware, - "./routes/index.tsx": $index, - }, - islands: {}, - baseUrl: import.meta.url, -} satisfies Manifest; - -export default manifest; diff --git a/tests/fixture_tailwind/main.ts b/tests/fixture_tailwind/main.ts deleted file mode 100644 index fc9359215e3..00000000000 --- a/tests/fixture_tailwind/main.ts +++ /dev/null @@ -1,11 +0,0 @@ -/// -/// -/// -/// -/// - -import { start } from "$fresh/server.ts"; -import manifest from "./fresh.gen.ts"; -import config from "./fresh.config.ts"; - -await start(manifest, config); diff --git a/tests/fixture_tailwind/routes/_app.tsx b/tests/fixture_tailwind/routes/_app.tsx deleted file mode 100644 index 2e225e497f4..00000000000 --- a/tests/fixture_tailwind/routes/_app.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { PageProps } from "$fresh/server.ts"; - -export default function App({ Component }: PageProps) { - return ( - - - - - My Fresh app - - - - - - - ); -} diff --git a/tests/fixture_tailwind/routes/_middleware.ts b/tests/fixture_tailwind/routes/_middleware.ts deleted file mode 100644 index 362a96be86b..00000000000 --- a/tests/fixture_tailwind/routes/_middleware.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { FreshContext } from "$fresh/server.ts"; - -export async function handler( - _req: Request, - ctx: FreshContext, -) { - if (ctx.url.pathname === "/middleware-only.css") { - return new Response(".foo-bar { color: red }", { - headers: { - "Content-Type": "text/css", - }, - }); - } - return await ctx.next(); -} diff --git a/tests/fixture_tailwind/routes/index.tsx b/tests/fixture_tailwind/routes/index.tsx deleted file mode 100644 index ee3f8f3c9bd..00000000000 --- a/tests/fixture_tailwind/routes/index.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export default function Page() { - return

foo

; -} diff --git a/tests/fixture_tailwind/static/styles.css b/tests/fixture_tailwind/static/styles.css deleted file mode 100644 index b5c61c95671..00000000000 --- a/tests/fixture_tailwind/static/styles.css +++ /dev/null @@ -1,3 +0,0 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; diff --git a/tests/fixture_tailwind/tailwind.config.ts b/tests/fixture_tailwind/tailwind.config.ts deleted file mode 100644 index d6a93ea01a1..00000000000 --- a/tests/fixture_tailwind/tailwind.config.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Config } from "tailwindcss"; - -export default { - content: [ - "{routes,islands,components}/**/*.{ts,tsx}", - ], -} satisfies Config; diff --git a/tests/fixture_tailwind_build/deno.json b/tests/fixture_tailwind_build/deno.json deleted file mode 100644 index 6d4529cc180..00000000000 --- a/tests/fixture_tailwind_build/deno.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "lock": false, - "imports": { - "$fresh/": "../../", - "preact": "https://esm.sh/preact@10.22.0", - "preact/": "https://esm.sh/preact@10.22.0/", - "tailwindcss": "npm:tailwindcss@3.4.1" - }, - "compilerOptions": { - "jsx": "react-jsx", - "jsxImportSource": "preact" - } -} diff --git a/tests/fixture_tailwind_build/dev.ts b/tests/fixture_tailwind_build/dev.ts deleted file mode 100755 index 1fe3e340282..00000000000 --- a/tests/fixture_tailwind_build/dev.ts +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env -S deno run -A --watch=static/,routes/ - -import dev from "$fresh/dev.ts"; -import config from "./fresh.config.ts"; - -await dev(import.meta.url, "./main.ts", config); diff --git a/tests/fixture_tailwind_build/fresh.config.ts b/tests/fixture_tailwind_build/fresh.config.ts deleted file mode 100644 index 6f7acca6964..00000000000 --- a/tests/fixture_tailwind_build/fresh.config.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { defineConfig } from "$fresh/server.ts"; -import tailwind from "$fresh/plugins/tailwind.ts"; - -export default defineConfig({ - plugins: [ - tailwind(), - ], -}); diff --git a/tests/fixture_tailwind_build/fresh.gen.ts b/tests/fixture_tailwind_build/fresh.gen.ts deleted file mode 100644 index 9c108e56c68..00000000000 --- a/tests/fixture_tailwind_build/fresh.gen.ts +++ /dev/null @@ -1,19 +0,0 @@ -// DO NOT EDIT. This file is generated by Fresh. -// This file SHOULD be checked into source version control. -// This file is automatically updated during development when running `dev.ts`. - -import * as $_app from "./routes/_app.tsx"; -import * as $index from "./routes/index.tsx"; - -import type { Manifest } from "$fresh/server.ts"; - -const manifest = { - routes: { - "./routes/_app.tsx": $_app, - "./routes/index.tsx": $index, - }, - islands: {}, - baseUrl: import.meta.url, -} satisfies Manifest; - -export default manifest; diff --git a/tests/fixture_tailwind_build/main.ts b/tests/fixture_tailwind_build/main.ts deleted file mode 100644 index fc9359215e3..00000000000 --- a/tests/fixture_tailwind_build/main.ts +++ /dev/null @@ -1,11 +0,0 @@ -/// -/// -/// -/// -/// - -import { start } from "$fresh/server.ts"; -import manifest from "./fresh.gen.ts"; -import config from "./fresh.config.ts"; - -await start(manifest, config); diff --git a/tests/fixture_tailwind_build/routes/_app.tsx b/tests/fixture_tailwind_build/routes/_app.tsx deleted file mode 100644 index 2e225e497f4..00000000000 --- a/tests/fixture_tailwind_build/routes/_app.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { PageProps } from "$fresh/server.ts"; - -export default function App({ Component }: PageProps) { - return ( - - - - - My Fresh app - - - - - - - ); -} diff --git a/tests/fixture_tailwind_build/routes/index.tsx b/tests/fixture_tailwind_build/routes/index.tsx deleted file mode 100644 index ee3f8f3c9bd..00000000000 --- a/tests/fixture_tailwind_build/routes/index.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export default function Page() { - return

foo

; -} diff --git a/tests/fixture_tailwind_build/static/styles.css b/tests/fixture_tailwind_build/static/styles.css deleted file mode 100644 index b5c61c95671..00000000000 --- a/tests/fixture_tailwind_build/static/styles.css +++ /dev/null @@ -1,3 +0,0 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; diff --git a/tests/fixture_tailwind_build/tailwind.config.ts b/tests/fixture_tailwind_build/tailwind.config.ts deleted file mode 100644 index d6a93ea01a1..00000000000 --- a/tests/fixture_tailwind_build/tailwind.config.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Config } from "tailwindcss"; - -export default { - content: [ - "{routes,islands,components}/**/*.{ts,tsx}", - ], -} satisfies Config; diff --git a/tests/fixture_tailwind_build_2/deno.json b/tests/fixture_tailwind_build_2/deno.json deleted file mode 100644 index 6d4529cc180..00000000000 --- a/tests/fixture_tailwind_build_2/deno.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "lock": false, - "imports": { - "$fresh/": "../../", - "preact": "https://esm.sh/preact@10.22.0", - "preact/": "https://esm.sh/preact@10.22.0/", - "tailwindcss": "npm:tailwindcss@3.4.1" - }, - "compilerOptions": { - "jsx": "react-jsx", - "jsxImportSource": "preact" - } -} diff --git a/tests/fixture_tailwind_build_2/dev.ts b/tests/fixture_tailwind_build_2/dev.ts deleted file mode 100755 index 1fe3e340282..00000000000 --- a/tests/fixture_tailwind_build_2/dev.ts +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env -S deno run -A --watch=static/,routes/ - -import dev from "$fresh/dev.ts"; -import config from "./fresh.config.ts"; - -await dev(import.meta.url, "./main.ts", config); diff --git a/tests/fixture_tailwind_build_2/fresh.config.ts b/tests/fixture_tailwind_build_2/fresh.config.ts deleted file mode 100644 index 6f7acca6964..00000000000 --- a/tests/fixture_tailwind_build_2/fresh.config.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { defineConfig } from "$fresh/server.ts"; -import tailwind from "$fresh/plugins/tailwind.ts"; - -export default defineConfig({ - plugins: [ - tailwind(), - ], -}); diff --git a/tests/fixture_tailwind_build_2/fresh.gen.ts b/tests/fixture_tailwind_build_2/fresh.gen.ts deleted file mode 100644 index 9c108e56c68..00000000000 --- a/tests/fixture_tailwind_build_2/fresh.gen.ts +++ /dev/null @@ -1,19 +0,0 @@ -// DO NOT EDIT. This file is generated by Fresh. -// This file SHOULD be checked into source version control. -// This file is automatically updated during development when running `dev.ts`. - -import * as $_app from "./routes/_app.tsx"; -import * as $index from "./routes/index.tsx"; - -import type { Manifest } from "$fresh/server.ts"; - -const manifest = { - routes: { - "./routes/_app.tsx": $_app, - "./routes/index.tsx": $index, - }, - islands: {}, - baseUrl: import.meta.url, -} satisfies Manifest; - -export default manifest; diff --git a/tests/fixture_tailwind_build_2/main.ts b/tests/fixture_tailwind_build_2/main.ts deleted file mode 100644 index fc9359215e3..00000000000 --- a/tests/fixture_tailwind_build_2/main.ts +++ /dev/null @@ -1,11 +0,0 @@ -/// -/// -/// -/// -/// - -import { start } from "$fresh/server.ts"; -import manifest from "./fresh.gen.ts"; -import config from "./fresh.config.ts"; - -await start(manifest, config); diff --git a/tests/fixture_tailwind_build_2/routes/_app.tsx b/tests/fixture_tailwind_build_2/routes/_app.tsx deleted file mode 100644 index 1e34814798e..00000000000 --- a/tests/fixture_tailwind_build_2/routes/_app.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { PageProps } from "$fresh/server.ts"; - -export default function App({ Component }: PageProps) { - return ( - - - - - My Fresh app - - - - - - - ); -} diff --git a/tests/fixture_tailwind_build_2/routes/index.tsx b/tests/fixture_tailwind_build_2/routes/index.tsx deleted file mode 100644 index ee3f8f3c9bd..00000000000 --- a/tests/fixture_tailwind_build_2/routes/index.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export default function Page() { - return

foo

; -} diff --git a/tests/fixture_tailwind_build_2/static/foo/styles.css b/tests/fixture_tailwind_build_2/static/foo/styles.css deleted file mode 100644 index b5c61c95671..00000000000 --- a/tests/fixture_tailwind_build_2/static/foo/styles.css +++ /dev/null @@ -1,3 +0,0 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; diff --git a/tests/fixture_tailwind_build_2/tailwind.config.ts b/tests/fixture_tailwind_build_2/tailwind.config.ts deleted file mode 100644 index d6a93ea01a1..00000000000 --- a/tests/fixture_tailwind_build_2/tailwind.config.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Config } from "tailwindcss"; - -export default { - content: [ - "{routes,islands,components}/**/*.{ts,tsx}", - ], -} satisfies Config; diff --git a/tests/fixture_tailwind_config/deno.json b/tests/fixture_tailwind_config/deno.json deleted file mode 100644 index 6d4529cc180..00000000000 --- a/tests/fixture_tailwind_config/deno.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "lock": false, - "imports": { - "$fresh/": "../../", - "preact": "https://esm.sh/preact@10.22.0", - "preact/": "https://esm.sh/preact@10.22.0/", - "tailwindcss": "npm:tailwindcss@3.4.1" - }, - "compilerOptions": { - "jsx": "react-jsx", - "jsxImportSource": "preact" - } -} diff --git a/tests/fixture_tailwind_config/dev.ts b/tests/fixture_tailwind_config/dev.ts deleted file mode 100755 index 1fe3e340282..00000000000 --- a/tests/fixture_tailwind_config/dev.ts +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env -S deno run -A --watch=static/,routes/ - -import dev from "$fresh/dev.ts"; -import config from "./fresh.config.ts"; - -await dev(import.meta.url, "./main.ts", config); diff --git a/tests/fixture_tailwind_config/foo/Foo.tsx b/tests/fixture_tailwind_config/foo/Foo.tsx deleted file mode 100644 index 965dddb6b17..00000000000 --- a/tests/fixture_tailwind_config/foo/Foo.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export function Foo() { - return

foo

; -} diff --git a/tests/fixture_tailwind_config/fresh.config.ts b/tests/fixture_tailwind_config/fresh.config.ts deleted file mode 100644 index 6f7acca6964..00000000000 --- a/tests/fixture_tailwind_config/fresh.config.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { defineConfig } from "$fresh/server.ts"; -import tailwind from "$fresh/plugins/tailwind.ts"; - -export default defineConfig({ - plugins: [ - tailwind(), - ], -}); diff --git a/tests/fixture_tailwind_config/fresh.gen.ts b/tests/fixture_tailwind_config/fresh.gen.ts deleted file mode 100644 index e6643ad216e..00000000000 --- a/tests/fixture_tailwind_config/fresh.gen.ts +++ /dev/null @@ -1,19 +0,0 @@ -// DO NOT EDIT. This file is generated by Fresh. -// This file SHOULD be checked into source version control. -// This file is automatically updated during development when running `dev.ts`. - -import * as $_app from "./routes/_app.tsx"; -import * as $index from "./routes/index.tsx"; - -import { type Manifest } from "$fresh/server.ts"; - -const manifest = { - routes: { - "./routes/_app.tsx": $_app, - "./routes/index.tsx": $index, - }, - islands: {}, - baseUrl: import.meta.url, -} satisfies Manifest; - -export default manifest; diff --git a/tests/fixture_tailwind_config/main.ts b/tests/fixture_tailwind_config/main.ts deleted file mode 100644 index fc9359215e3..00000000000 --- a/tests/fixture_tailwind_config/main.ts +++ /dev/null @@ -1,11 +0,0 @@ -/// -/// -/// -/// -/// - -import { start } from "$fresh/server.ts"; -import manifest from "./fresh.gen.ts"; -import config from "./fresh.config.ts"; - -await start(manifest, config); diff --git a/tests/fixture_tailwind_config/routes/_app.tsx b/tests/fixture_tailwind_config/routes/_app.tsx deleted file mode 100644 index 2e225e497f4..00000000000 --- a/tests/fixture_tailwind_config/routes/_app.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { PageProps } from "$fresh/server.ts"; - -export default function App({ Component }: PageProps) { - return ( - - - - - My Fresh app - - - - - - - ); -} diff --git a/tests/fixture_tailwind_config/routes/index.tsx b/tests/fixture_tailwind_config/routes/index.tsx deleted file mode 100644 index d69dd510528..00000000000 --- a/tests/fixture_tailwind_config/routes/index.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { Foo } from "../foo/Foo.tsx"; - -export default function Page() { - return ; -} diff --git a/tests/fixture_tailwind_config/static/styles.css b/tests/fixture_tailwind_config/static/styles.css deleted file mode 100644 index b5c61c95671..00000000000 --- a/tests/fixture_tailwind_config/static/styles.css +++ /dev/null @@ -1,3 +0,0 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; diff --git a/tests/fixture_tailwind_config/tailwind.config.ts b/tests/fixture_tailwind_config/tailwind.config.ts deleted file mode 100644 index eaef5a3a7c8..00000000000 --- a/tests/fixture_tailwind_config/tailwind.config.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** @type {import('tailwindcss').Config} */ -export default { - content: ["./foo/**/*.{ts,tsx}"], - theme: { - extend: { - colors: { - pp: "peachpuff", - }, - }, - }, -}; diff --git a/tests/fixture_twind_app/deno.json b/tests/fixture_twind_app/deno.json deleted file mode 100644 index 911a397bdea..00000000000 --- a/tests/fixture_twind_app/deno.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "lock": false, - "tasks": { - "start": "deno run -A --watch=static/,routes/ dev.ts" - }, - "imports": { - "$fresh/": "../../", - "twind": "https://esm.sh/twind@0.16.19", - "twind/": "https://esm.sh/twind@0.16.19/", - "preact": "https://esm.sh/preact@10.22.0", - "preact/": "https://esm.sh/preact@10.22.0/", - "@preact/signals": "https://esm.sh/*@preact/signals@1.1.3", - "@preact/signals-core": "https://esm.sh/@preact/signals-core@1.2.3" - }, - "compilerOptions": { - "jsx": "react-jsx", - "jsxImportSource": "preact" - } -} diff --git a/tests/fixture_twind_app/dev.ts b/tests/fixture_twind_app/dev.ts deleted file mode 100755 index 2d85d6c183c..00000000000 --- a/tests/fixture_twind_app/dev.ts +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env -S deno run -A --watch=static/,routes/ - -import dev from "$fresh/dev.ts"; - -await dev(import.meta.url, "./main.ts"); diff --git a/tests/fixture_twind_app/fresh.gen.ts b/tests/fixture_twind_app/fresh.gen.ts deleted file mode 100644 index 0eba3d64f58..00000000000 --- a/tests/fixture_twind_app/fresh.gen.ts +++ /dev/null @@ -1,21 +0,0 @@ -// DO NOT EDIT. This file is generated by Fresh. -// This file SHOULD be checked into source version control. -// This file is automatically updated during development when running `dev.ts`. - -import * as $_app from "./routes/_app.tsx"; -import * as $app_class from "./routes/app_class.tsx"; -import * as $index from "./routes/index.tsx"; - -import { type Manifest } from "$fresh/server.ts"; - -const manifest = { - routes: { - "./routes/_app.tsx": $_app, - "./routes/app_class.tsx": $app_class, - "./routes/index.tsx": $index, - }, - islands: {}, - baseUrl: import.meta.url, -} satisfies Manifest; - -export default manifest; diff --git a/tests/fixture_twind_app/main.ts b/tests/fixture_twind_app/main.ts deleted file mode 100644 index 81b6ecb5bd4..00000000000 --- a/tests/fixture_twind_app/main.ts +++ /dev/null @@ -1,18 +0,0 @@ -/// -/// -/// -/// - -import { start } from "$fresh/server.ts"; -import twind from "$fresh/plugins/twind.ts"; -import twindV1 from "$fresh/plugins/twindv1.ts"; -import manifest from "./fresh.gen.ts"; - -const twindPlugin = Deno.env.has("TWIND_V1") - ? twindV1({ - selfURL: import.meta.url, - // deno-lint-ignore no-explicit-any - } as any) - : twind({ selfURL: import.meta.url }); - -await start(manifest, { plugins: [twindPlugin] }); diff --git a/tests/fixture_twind_app/routes/_app.tsx b/tests/fixture_twind_app/routes/_app.tsx deleted file mode 100644 index af08da677dd..00000000000 --- a/tests/fixture_twind_app/routes/_app.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { PageProps } from "$fresh/server.ts"; - -export default function App({ Component }: PageProps) { - return ( - - - - - fresh-foo - - - - - - ); -} diff --git a/tests/fixture_twind_app/routes/app_class.tsx b/tests/fixture_twind_app/routes/app_class.tsx deleted file mode 100644 index 5048322b358..00000000000 --- a/tests/fixture_twind_app/routes/app_class.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export default function Page() { - return

it works

; -} diff --git a/tests/fixture_twind_app/routes/index.tsx b/tests/fixture_twind_app/routes/index.tsx deleted file mode 100644 index 24478694fc0..00000000000 --- a/tests/fixture_twind_app/routes/index.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { basename, dirname, extname, fromFileUrl } from "../../deps.ts"; - -const __dirname = dirname(fromFileUrl(import.meta.url)); - -const links: string[] = []; -for (const file of Deno.readDirSync(__dirname)) { - if (file.name.startsWith("index")) continue; - const name = basename(file.name, extname(file.name)); - links.push(name); -} - -export default function Home() { - return ( -
-

Tests

-
    - {links.map((link) => { - return ( -
  • - {link} -
  • - ); - })} -
-
- ); -} diff --git a/tests/fixture_twind_hydrate/deno.json b/tests/fixture_twind_hydrate/deno.json deleted file mode 100644 index 81c87e731f4..00000000000 --- a/tests/fixture_twind_hydrate/deno.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "lock": false, - "tasks": { - "start": "deno run -A --watch=static/,routes/ dev.ts" - }, - "imports": { - "$fresh/": "../../", - "preact": "https://esm.sh/preact@10.11.0", - "preact/": "https://esm.sh/preact@10.11.0/", - "@preact/signals": "https://esm.sh/*@preact/signals@1.0.3", - "@preact/signals-core": "https://esm.sh/*@preact/signals-core@1.0.1" - }, - "compilerOptions": { - "jsx": "react-jsx", - "jsxImportSource": "preact" - } -} diff --git a/tests/fixture_twind_hydrate/dev.ts b/tests/fixture_twind_hydrate/dev.ts deleted file mode 100755 index 2d85d6c183c..00000000000 --- a/tests/fixture_twind_hydrate/dev.ts +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env -S deno run -A --watch=static/,routes/ - -import dev from "$fresh/dev.ts"; - -await dev(import.meta.url, "./main.ts"); diff --git a/tests/fixture_twind_hydrate/fresh.config.ts b/tests/fixture_twind_hydrate/fresh.config.ts deleted file mode 100644 index 8d2c1635d61..00000000000 --- a/tests/fixture_twind_hydrate/fresh.config.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { FreshConfig } from "$fresh/server.ts"; -import twindPlugin from "../../plugins/twindv1.ts"; -import twindConfig from "./twind.config.ts"; - -export default { plugins: [twindPlugin(twindConfig)] } as FreshConfig; diff --git a/tests/fixture_twind_hydrate/fresh.gen.ts b/tests/fixture_twind_hydrate/fresh.gen.ts deleted file mode 100644 index dd4952a988c..00000000000 --- a/tests/fixture_twind_hydrate/fresh.gen.ts +++ /dev/null @@ -1,35 +0,0 @@ -// DO NOT EDIT. This file is generated by Fresh. -// This file SHOULD be checked into source version control. -// This file is automatically updated during development when running `dev.ts`. - -import * as $check_duplication from "./routes/check-duplication.tsx"; -import * as $insert_cssrules from "./routes/insert-cssrules.tsx"; -import * as $island_twind_layout from "./routes/island_twind/_layout.tsx"; -import * as $island_twind_blue from "./routes/island_twind/blue.tsx"; -import * as $island_twind_index from "./routes/island_twind/index.tsx"; -import * as $static from "./routes/static.tsx"; -import * as $unused from "./routes/unused.tsx"; -import * as $unused_tw from "./routes/unused_tw.tsx"; -import * as $CheckDuplication from "./islands/CheckDuplication.tsx"; -import * as $InsertCssrules from "./islands/InsertCssrules.tsx"; -import { type Manifest } from "$fresh/server.ts"; - -const manifest = { - routes: { - "./routes/check-duplication.tsx": $check_duplication, - "./routes/insert-cssrules.tsx": $insert_cssrules, - "./routes/island_twind/_layout.tsx": $island_twind_layout, - "./routes/island_twind/blue.tsx": $island_twind_blue, - "./routes/island_twind/index.tsx": $island_twind_index, - "./routes/static.tsx": $static, - "./routes/unused.tsx": $unused, - "./routes/unused_tw.tsx": $unused_tw, - }, - islands: { - "./islands/CheckDuplication.tsx": $CheckDuplication, - "./islands/InsertCssrules.tsx": $InsertCssrules, - }, - baseUrl: import.meta.url, -} satisfies Manifest; - -export default manifest; diff --git a/tests/fixture_twind_hydrate/islands/CheckDuplication.tsx b/tests/fixture_twind_hydrate/islands/CheckDuplication.tsx deleted file mode 100644 index 08fc60ad5e9..00000000000 --- a/tests/fixture_twind_hydrate/islands/CheckDuplication.tsx +++ /dev/null @@ -1,62 +0,0 @@ -// https://github.com/denoland/fresh/pull/1050 -import { useEffect } from "preact/hooks"; -import { cmpCssRules } from "../utils/utils.ts"; -import { useSignal } from "@preact/signals"; - -/** - * Returns a cssrulelist of styleElement matching the selector. - */ -function getCssrules(selector: string) { - const elem = document.querySelector(selector) as HTMLStyleElement; - return elem?.sheet?.cssRules; -} - -export default function CheckDuplication() { - const cssRulesFRSHTWIND = useSignal(undefined); - const cssRulesClaimed = useSignal(undefined); - - // Init - useEffect(() => { - // get + + + +

updated heading

+

+ updated +

+
+ + , + ); + }) + .get("/", (ctx) => { + return ctx.render( + + + {charset} + {favicon} + Head merge + + + + + + +

+ init +

+
+

+ +

+ + , + ); + }); + + await withBrowserApp(app, async (page, address) => { + await page.goto(address, { waitUntil: "load" }); + + await page.locator(".update").click(); + await page.locator(".updated").wait(); + + await waitFor(async () => { + return (await page.evaluate(() => document.title)) === + "Head merge updated"; + }); + + const doc = parseHtml(await page.content()); + expect(doc.title).toEqual("Head merge updated"); + + assertMetaContent(doc, "foo", "bar baz"); + assertMetaContent(doc, "og:foo", "og value foo"); + assertMetaContent(doc, "og:bar", "og value bar"); + + const color = await page + .locator("h1") + .evaluate((el) => { + return globalThis.getComputedStyle(el).color; + }); + expect(color).toEqual("rgb(255, 0, 0)"); + + const textColor = await page + .locator("p") + .evaluate((el) => { + return globalThis.getComputedStyle(el).color; + }); + expect(textColor).toEqual("rgb(0, 128, 0)"); + }); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "partials - does not merge duplicate content", + fn: async () => { + const app = testApp() + .get("/style.css", () => + new Response("h1 { color: red }", { + headers: { + "Content-Type": "text/css", + }, + })) + .get("/partial", (ctx) => { + return ctx.render( + + + {charset} + {favicon} + Head merge duplicated + + + + + + + +

+ updated +

+
+

+ +

+ + , + ); + }) + .get("/", (ctx) => { + return ctx.render( + + + {charset} + {favicon} + Head merge + + + + + + + +

+ init +

+
+

+ +

+ + , + ); + }); + + await withBrowserApp(app, async (page, address) => { + await page.goto(address, { waitUntil: "load" }); + await page.locator(".update").click(); + await page.locator(".updated").wait(); + + await waitFor(async () => { + return (await page.evaluate(() => document.title)) === + "Head merge duplicated"; + }); + + const html = await page.content(); + expect( + Array.from(html.matchAll(/id="style-foo"/g)).length === 1, + ).toEqual(true); + + expect( + Array.from(html.matchAll(/style\.css/g)).length === 1, + ).toEqual(true); + }); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "partials - supports relative links", + fn: async () => { + const app = testApp() + .get("/", (ctx) => { + const { searchParams } = ctx.url; + return ctx.render( + +
+ +

+ {searchParams.has("refresh") + ? "Refreshed content" + : "Initial content"} +

+
+

+ +

+
+
, + ); + }); + + await withBrowserApp(app, async (page, address) => { + await page.goto(address, { waitUntil: "load" }); + await page.locator(".status-initial").wait(); + + await page.locator("button").click(); + await page.locator(".status-refreshed").wait(); + }); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "partials - update stateful inner partials", + fn: async () => { + const app = testApp() + .get("/partial", (ctx) => { + return ctx.render( + + +

done

+ +
+
, + ); + }) + .get("/", (ctx) => { + return ctx.render( + +
+ + + + +

init

+ +
+
+
+
, + ); + }); + + await withBrowserApp(app, async (page, address) => { + await page.goto(address, { waitUntil: "load" }); + await page.locator(".ready").wait(); + await page.locator("#outer .increment").click(); + await page.locator("#outer .increment").click(); + + await page.locator("#inner .increment").click(); + + await waitForText(page, "#outer .output", "2"); + await waitForText(page, "#inner .output", "1"); + + await page.locator(".update").click(); + await page.locator(".done").wait(); + + await waitForText(page, "#outer .output", "2"); + await waitForText(page, "#inner .output", "1"); + }); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "partials - with redirects", + fn: async () => { + const app = testApp() + .get("/a", (ctx) => { + return ctx.render( + + +

foo update

+
+
, + ); + }) + .get("/partial", (ctx) => ctx.redirect("/a")) + .get("/", (ctx) => { + return ctx.render( + +
+ + +

foo

+
+
+
, + ); + }); + + await withBrowserApp(app, async (page, address) => { + await page.goto(address, { waitUntil: "load" }); + await page.locator("h1").wait(); + await page.locator(".update").click(); + await page.locator(".done").wait(); + }); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "partials - render 404 partial", + fn: async () => { + const app = testApp() + .get("/", (ctx) => { + return ctx.render( + +
+ + +

foo

+
+
+
, + ); + }) + .get("*", (ctx) => { + return ctx.render( + + +

404

+
+
, + ); + }); + + await withBrowserApp(app, async (page, address) => { + await page.goto(address, { waitUntil: "load" }); + await page.locator("h1").wait(); + await page.locator(".update").click(); + await page.locator(".error-404").wait(); + }); + }, + sanitizeResources: false, + sanitizeOps: false, +}); + +Deno.test({ + name: "partials - render with new title", + fn: async () => { + const app = testApp() + .get("/partial", (ctx) => { + return ctx.render( + + +

foo update

+
+
, + ); + }) + .get("/", (ctx) => { + return ctx.render( + +
+ + +

foo

+
+
+
, + ); + }); + + await withBrowserApp(app, async (page, address) => { + await page.goto(address, { waitUntil: "load" }); + await page.locator("h1").wait(); + await page.locator(".update").click(); + await page.locator(".done").wait(); + + const title = await page.evaluate(() => document.title); + expect(title).toEqual("after update"); + }); + }, + sanitizeResources: false, + sanitizeOps: false, +}); diff --git a/tests/plugin_test.ts b/tests/plugin_test.ts deleted file mode 100644 index 8e105ce9ca2..00000000000 --- a/tests/plugin_test.ts +++ /dev/null @@ -1,287 +0,0 @@ -import { type FreshConfig, ServerContext, STATUS_CODE } from "../server.ts"; -import { - assert, - assertEquals, - assertMatch, - assertStringIncludes, - delay, - join, - puppeteer, -} from "./deps.ts"; -import manifest from "./fixture_plugin/fresh.gen.ts"; -import config from "./fixture_plugin/fresh.config.ts"; -import { - clickWhenListenerReady, - runBuild, - startFreshServer, - withFakeServe, - withPageName, -} from "./test_utils.ts"; -import routePlugin from "./fixture_plugin/utils/route-plugin.ts"; -import secondMiddlewarePlugin from "./fixture_plugin/utils/second-middleware-plugin.ts"; - -const ctx = await ServerContext.fromManifest(manifest, config); -const handler = ctx.handler(); -const router = (req: Request) => { - return handler(req, { - remoteAddr: { - transport: "tcp", - hostname: "127.0.0.1", - port: 80, - }, - // deno-lint-ignore no-explicit-any - } as any); -}; - -Deno.test("/static page prerender", async () => { - const resp = await router(new Request("https://fresh.deno.dev/static")); - assert(resp); - assertEquals(resp.status, STATUS_CODE.OK); - const body = await resp.text(); - assertStringIncludes(body, ''); - assert(!body.includes(`>{"v":[[],[]]}`)); - assert(!body.includes(`import`)); - assertStringIncludes( - body, - '', - ); - assertStringIncludes(body, ''); - assertStringIncludes( - body, - '', - ); -}); - -Deno.test("/with-island prerender", async () => { - const resp = await router(new Request("https://fresh.deno.dev/with-island")); - assert(resp); - assertEquals(resp.status, STATUS_CODE.OK); - const body = await resp.text(); - assertStringIncludes( - body, - '', - ); - assertStringIncludes(body, `>{"v":[[{}],["JS injected!"]]}`); - assertStringIncludes(body, `/plugin-js-inject-main.js"`); - assertStringIncludes( - body, - '', - ); -}); - -Deno.test("plugin routes and middleware", async () => { - const resp = await router(new Request("https://fresh.deno.dev/test")); - assert(resp); - assertEquals(resp.status, STATUS_CODE.OK); - const body = await resp.text(); - assertStringIncludes( - body, - `

look, i'm set from a plugin!

`, - ); - assertStringIncludes( - body, - `Title Set From Plugin Config`, - ); -}); - -Deno.test("plugin routes and middleware -- async _app", async () => { - const ctx = await ServerContext.fromManifest(manifest, { - plugins: [ - routePlugin({ title: "Title Set From Plugin Config", async: true }), - secondMiddlewarePlugin(), - ], - } as FreshConfig); - const handler = ctx.handler(); - const router = (req: Request) => { - return handler(req, { - remoteAddr: { - transport: "tcp", - hostname: "127.0.0.1", - port: 80, - }, - // deno-lint-ignore no-explicit-any - } as any); - }; - - const resp = await router(new Request("https://fresh.deno.dev/test")); - assert(resp); - assertEquals(resp.status, STATUS_CODE.OK); - const body = await resp.text(); - assertStringIncludes( - body, - `

look, i'm set from a plugin!

`, - ); - assertStringIncludes( - body, - `Title Set From Plugin Config`, - ); -}); - -Deno.test("plugin middleware multiple handlers", async () => { - const resp = await router( - new Request("https://fresh.deno.dev/lots-of-middleware"), - ); - assert(resp); - assertEquals(resp.status, STATUS_CODE.OK); - const body = await resp.text(); - assertStringIncludes( - body, - `

3

`, - ); -}); - -Deno.test("plugin route no leading slash", async () => { - const resp = await router( - new Request("https://fresh.deno.dev/no-leading-slash-here"), - ); - assert(resp); - assertEquals(resp.status, STATUS_CODE.OK); - const body = await resp.text(); - assertStringIncludes( - body, - `
Hello
`, - ); -}); - -Deno.test("plugin async route", async () => { - const resp = await router( - new Request("https://fresh.deno.dev/async-route"), - ); - assert(resp); - assertEquals(resp.status, STATUS_CODE.OK); - const body = await resp.text(); - assertStringIncludes( - body, - `
this is an async route!
`, - ); -}); - -Deno.test({ - name: "plugin supports islands", - async fn(t) { - await withPageName( - "./tests/fixture_plugin/main.ts", - async (page, address) => { - async function idTest(id: string) { - const elem = await page.waitForSelector(`#${id}`); - - const value = await elem?.evaluate((el) => el.textContent); - assert(value === `${id}`, `value ${value} not equal to id ${id}`); - } - - await page.goto(`${address}/pluginroutewithisland`, { - waitUntil: "networkidle2", - }); - - await t.step("verify tags", async () => { - await idTest("csr"); - await idTest("csr_alt_folder"); - }); - }, - ); - }, - sanitizeOps: false, - sanitizeResources: false, -}); - -Deno.test({ - name: "/with-island hydration", - async fn(t) { - // Preparation - const { lines, serverProcess, address } = await startFreshServer({ - args: ["run", "-A", "./tests/fixture_plugin/main.ts"], - }); - - await delay(100); - - const browser = await puppeteer.launch({ args: ["--no-sandbox"] }); - const page = await browser.newPage(); - - await page.goto(`${address}/with-island`, { - waitUntil: "networkidle2", - }); - - await t.step("island is revived", async () => { - await page.waitForSelector("#csr"); - }); - - await t.step("title was updated", async () => { - const title = await page.title(); - assertEquals(title, "JS injected!"); - }); - - await browser.close(); - - serverProcess.kill("SIGTERM"); - await serverProcess.status; - - // Drain the lines stream - for await (const _ of lines) { /* noop */ } - }, -}); - -Deno.test("calls buildStart() and buildEnd()", async () => { - const result = await runBuild("./tests/fixture_plugin_lifecycle/dev.ts"); - - const out = result.stdout.split("\n").filter((line) => - line.startsWith("Plugin") - ); - - assertEquals(out, [ - "Plugin a: configResolved", - "Plugin b: configResolved", - "Plugin c: configResolved", - "Plugin a: buildStart", - "Plugin b: buildStart", - `Plugin c: ${join("tests", "fixture_plugin_lifecycle", "_fresh")}`, - "Plugin a: buildEnd", - "Plugin b: buildEnd", - ]); -}); - -Deno.test("calls configResolved() in dev", async () => { - await withFakeServe( - "./tests/fixture_plugin_resolved_dev/dev.ts", - async (server) => { - const res = await server.get("/"); - await res.text(); - assertEquals(res.headers.get("X-Plugin-A"), "true"); - }, - { loadConfig: true }, - ); -}); - -Deno.test("plugin script doesn't halt island execution", async () => { - await withPageName( - "./tests/fixture_plugin_error/main.ts", - async (page, address) => { - let error; - page.on("pageerror", (err) => { - error = err; - }); - await page.goto(address); - await page.waitForSelector("#ready"); - - let text = await page.$eval("p", (el) => el.textContent!); - assertEquals(text, "0"); - - await clickWhenListenerReady(page, "button"); - - text = await page.$eval("p", (el) => el.textContent!); - assertEquals(text, "1"); - - assertMatch(String(error), /Error thrown/); - }, - ); -}); - -Deno.test("supports returning htmlText", async () => { - await withFakeServe( - "./tests/fixture_plugin_html/main.ts", - async (server) => { - const doc = await server.getHtml("/"); - assertEquals(doc.body.textContent, "it works"); - }, - { loadConfig: true }, - ); -}); diff --git a/tests/precompile_test.ts b/tests/precompile_test.ts new file mode 100644 index 00000000000..e4f9ac21e7f --- /dev/null +++ b/tests/precompile_test.ts @@ -0,0 +1,37 @@ +import * as path from "@std/path"; +import { expect } from "@std/expect"; + +Deno.test("JSX precompile - check config", async () => { + const cwd = path.join(import.meta.dirname!, "fixture_precompile", "invalid"); + const output = await new Deno.Command(Deno.execPath(), { + args: [ + "run", + "-A", + path.join(cwd, "dev.ts"), + ], + cwd, + }).output(); + + const stderr = new TextDecoder().decode(output.stderr); + expect(stderr).toContain("jsxPrecompileSkipElements to contain"); + expect(output.code).toEqual(1); +}); + +Deno.test("JSX precompile - run vnode hooks", async () => { + const cwd = path.join(import.meta.dirname!, "fixture_precompile", "valid"); + const output = await new Deno.Command(Deno.execPath(), { + args: [ + "run", + "-A", + path.join(cwd, "main.tsx"), + ], + cwd, + }).output(); + + const stdout = new TextDecoder().decode(output.stdout); + expect(stdout).toContain(''); + expect(stdout).toContain(''); + expect(output.code).toEqual(0); +}); diff --git a/tests/render_test.ts b/tests/render_test.ts deleted file mode 100644 index 703fb155905..00000000000 --- a/tests/render_test.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { - assertSelector, - assertTextMany, - parseHtml, - withFakeServe, -} from "./test_utils.ts"; -import { assertEquals } from "./deps.ts"; -import { createHandler } from "../server.ts"; -import manifest from "./fixture/fresh.gen.ts"; -import config from "./fixture/fresh.config.ts"; - -const handler = await createHandler(manifest, config); - -// Issue: https://github.com/denoland/fresh/issues/1636 -Deno.test("doesn't leak data across renderers", async () => { - async function load(name: string) { - const req = new Request(`http://localhost/admin/${name}`); - const resp = await handler(req); - const doc = parseHtml(await resp.text()); - - assertSelector(doc, "[id^=__FRSH_STATE]"); - const text = doc.querySelector("[id^=__FRSH_STATE]")?.textContent!; - const json = JSON.parse(text); - assertEquals(json, { "v": [[{ "site": name }], []] }); - } - - const promises = []; - for (let i = 0; i < 100; i++) { - promises.push(load("foo")); - promises.push(load("bar")); - } - await Promise.all(promises); -}); - -Deno.test("render headers passed to ctx.render()", async (t) => { - await withFakeServe("./tests/fixture_render/main.ts", async (server) => { - await t.step("header_arr", async () => { - const res = await server.get("/header_arr"); - assertEquals(res.headers.get("x-foo"), "Hello world!"); - await res.body?.cancel(); - }); - - await t.step("header_obj", async () => { - const res = await server.get("/header_obj"); - assertEquals(res.headers.get("x-foo"), "Hello world!"); - await res.body?.cancel(); - }); - - await t.step("header_instance", async () => { - const res = await server.get("/header_instance"); - assertEquals(res.headers.get("x-foo"), "Hello world!"); - await res.body?.cancel(); - }); - }); -}); - -Deno.test("render head text nodes", async () => { - await withFakeServe("./tests/fixture_render/main.ts", async (server) => { - const doc = await server.getHtml("/head_style"); - assertTextMany(doc, "style", ["body { color: red }"]); - assertEquals(doc.body.textContent, "hello"); - }); -}); - -Deno.test("support jsx precompile", async () => { - await withFakeServe( - "./tests/fixture_jsx_precompile/main.ts", - async (server) => { - const doc = await server.getHtml("/"); - assertTextMany(doc, "h1", ["Hello World"]); - assertTextMany(doc, ".island", ["it works"]); - }, - ); -}); - -Deno.test("support with jsx precompile", async () => { - await withFakeServe( - "./tests/fixture_jsx_precompile/main.ts", - async (server) => { - const doc = await server.getHtml("/head"); - assertTextMany(doc, "h1", ["Hello World"]); - assertTextMany(doc, "head title", ["foo"]); - }, - ); -}); - -Deno.test("Ensure manifest has valid specifiers", async () => { - await withFakeServe( - "./tests/fixture/main.ts", - async (server) => { - const doc = await server.getHtml("/foo.bar.baz"); - assertTextMany(doc, "p", ["it works"]); - }, - ); -}); - -Deno.test("render multiple set-cookie headers passed to ctx.render()", async () => { - await withFakeServe("./tests/fixture_render/dev.ts", async (server) => { - const res = await server.get("/cookiePasser"); - const cookies = res.headers.getSetCookie(); - assertEquals(cookies, ["foo=bar", "baz=1234"]); - await res.body?.cancel(); - }); -}); diff --git a/tests/route_analysis_test.ts b/tests/route_analysis_test.ts deleted file mode 100644 index 9efe4b549c5..00000000000 --- a/tests/route_analysis_test.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { startFreshServerExpectErrors } from "./test_utils.ts"; -import { dirname, join } from "$std/path/mod.ts"; -import { assertEquals, assertStringIncludes } from "./deps.ts"; -import { ServerContext } from "../server.ts"; -import manifest from "./fixture/fresh.gen.ts"; - -const dir = dirname(import.meta.url); - -Deno.test({ - name: "route-conflicts", - async fn() { - const errorMessage = await startFreshServerExpectErrors({ - args: ["run", "-A", join(dir, "./fixture_route_analysis/dev.ts")], - }); - assertStringIncludes( - errorMessage, - "Error: Route conflict detected. Multiple files have the same name", - ); - }, -}); - -Deno.test("match route parameter and static", async () => { - const handler = (await ServerContext.fromManifest(manifest, {})).handler(); - - const res = await handler( - new Request("https://fresh.deno.dev/movies/foo.json"), - ); - - assertEquals(await res.text(), "it works"); - assertEquals(res.status, 200); -}); - -Deno.test("match multiple route parameters", async () => { - const handler = (await ServerContext.fromManifest(manifest, {})).handler(); - - const res = await handler( - new Request("https://fresh.deno.dev/movies/foo@bar"), - ); - - assertEquals(await res.text(), "it works"); - assertEquals(res.status, 200); -}); diff --git a/tests/route_groups_test.ts b/tests/route_groups_test.ts deleted file mode 100644 index 72356ac1846..00000000000 --- a/tests/route_groups_test.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { assertEquals } from "./deps.ts"; -import { - assertTextMany, - parseHtml, - waitForText, - withFakeServe, - withPageName, -} from "./test_utils.ts"; - -Deno.test("applies only _layout file of one group", async () => { - await withFakeServe( - "./tests/fixture/main.ts", - async (server) => { - const doc = await server.getHtml(`/route-groups`); - - assertTextMany(doc, "p", ["Foo layout", "Foo page"]); - }, - ); -}); - -Deno.test("applies only _layout files in parent groups", async () => { - await withFakeServe( - "./tests/fixture/main.ts", - async (server) => { - const doc = await server.getHtml(`/route-groups/baz`); - assertTextMany(doc, "p", ["Bar layout", "Baz layout", "Baz page"]); - }, - ); -}); - -Deno.test("applies only _layout files in parent groups #2", async () => { - await withFakeServe( - "./tests/fixture/main.ts", - async (server) => { - const doc = await server.getHtml(`/route-groups/boof`); - assertTextMany(doc, "p", ["Bar layout", "Boof Page"]); - }, - ); -}); - -Deno.test("can co-locate islands inside routes folder", async () => { - await withPageName( - "./tests/fixture/main.ts", - async (page, address) => { - await page.goto(`${address}/route-groups-islands/`); - await page.waitForSelector("button"); - await page.click("button"); - await waitForText(page, "p", "1"); - }, - ); -}); - -Deno.test("does not treat files in (_islands) as routes", async () => { - await withFakeServe( - "./tests/fixture/main.ts", - async (server) => { - const res = await server.get(`/route-groups-islands/invalid`); - assertEquals(res.status, 404); - await res.body?.cancel(); - }, - ); -}); - -Deno.test("does not treat files in (_...) as routes", async () => { - await withFakeServe( - "./tests/fixture/main.ts", - async (server) => { - const res = await server.get(`/route-groups-islands/sub`); - assertEquals(res.status, 404); - await res.body?.cancel(); - }, - ); -}); - -Deno.test("resolve index route in group /(group)/index.tsx", async () => { - await withFakeServe( - "./tests/fixture_group_index/main.ts", - async (server) => { - const res = await server.get(`/`); - assertEquals(res.status, 200); - const doc = parseHtml(await res.text()); - assertEquals(doc.querySelector("h1")?.textContent, "it works"); - }, - ); -}); diff --git a/tests/server_components_test.ts b/tests/server_components_test.ts deleted file mode 100644 index a32cac6539a..00000000000 --- a/tests/server_components_test.ts +++ /dev/null @@ -1,191 +0,0 @@ -import { assertEquals } from "./deps.ts"; -import { - assertSelector, - assertTextMany, - withFakeServe, - withFresh, - withPageName, -} from "./test_utils.ts"; -import { STATUS_CODE } from "../server.ts"; - -Deno.test({ - name: "render async server component", - - async fn() { - await withFakeServe( - "./tests/fixture_server_components/main.ts", - async (server) => { - const doc = await server.getHtml(`/basic`); - assertTextMany(doc, "h1", ["it works"]); - }, - ); - }, -}); - -Deno.test({ - name: "uses returned response", - - async fn() { - await withFakeServe( - "./tests/fixture_server_components/main.ts", - async (server) => { - const res = await server.get(`/response`); - const text = await res.text(); - assertEquals(text, "it works"); - }, - ); - }, -}); - -Deno.test({ - name: "revives islands in async server component", - - async fn() { - await withPageName( - "./tests/fixture_server_components/main.ts", - async (page, address) => { - await page.goto(`${address}/island`); - - await page.waitForSelector("button"); - let text = await page.$eval("button", (el) => el.textContent); - assertEquals(text, "update 0"); - - await page.click("button"); - text = await page.$eval("button", (el) => el.textContent); - assertEquals(text, "update 1"); - }, - ); - }, -}); - -Deno.test("passes context to server component", async () => { - await withFresh( - "./tests/fixture_server_components/main.ts", - async (address) => { - const res = await fetch(`${address}/context/foo`); - const json = await res.json(); - - assertEquals(typeof json.localAddr, "object"); - assertEquals(typeof json.remoteAddr, "object"); - json.localAddr.port = 8000; - json.remoteAddr.port = 8000; - - assertEquals( - json, - { - localAddr: { - hostname: "localhost", - port: 8000, - transport: "tcp", - }, - remoteAddr: { - hostname: "127.0.0.1", - port: 8000, - transport: "tcp", - }, - config: { - basePath: "", - build: { - outDir: "tests/fixture_server_components/_fresh", - target: [ - "chrome99", - "firefox99", - "safari15", - ], - }, - dev: false, - plugins: [ - { entrypoints: {}, name: "twind", renderAsync: "AsyncFunction" }, - ], - render: "Function", - router: "", - server: {}, - staticDir: "tests/fixture_server_components/static", - }, - data: "", - error: "", - codeFrame: "", - pattern: "/context/:id", - render: "AsyncFunction", - Component: "Function", - destination: "route", - next: "Function", - basePath: "", - renderNotFound: "AsyncFunction", - url: `${address}/context/foo`, - route: "/context/:id", - params: { - id: "foo", - }, - state: {}, - isPartial: false, - }, - ); - }, - ); -}); - -Deno.test({ - name: "can call context.renderNotFound()", - - async fn() { - await withFakeServe( - "./tests/fixture_server_components/main.ts", - async (server) => { - const res = await server.get(`/fail`); - - assertEquals(res.status, STATUS_CODE.NotFound); - const html = await res.text(); - assertEquals(html, "Not found."); - }, - ); - }, -}); - -Deno.test({ - name: "works with async plugins", - - async fn() { - await withPageName( - "./tests/fixture_server_components/main.ts", - async (page, address) => { - await page.goto(`${address}/twind`); - await page.waitForSelector("h1"); - - const text = await page.$eval("h1", (el) => el.textContent); - assertEquals(text, "it works"); - - // Check that CSS was applied accordingly - const color = await page.$eval("h1", (el) => { - return globalThis.getComputedStyle(el).color; - }); - assertEquals(color, "rgb(220, 38, 38)"); - }, - ); - }, -}); - -Deno.test({ - name: "renders async app template", - - async fn() { - await withFakeServe( - "./tests/fixture_async_app/main.ts", - async (server) => { - const doc = await server.getHtml(``); - assertSelector(doc, "html > body > .app > .layout > .page"); - }, - ); - }, -}); - -Deno.test("define helpers", async () => { - await withFakeServe( - "./tests/fixture_define_helpers/main.ts", - async (server) => { - const doc = await server.getHtml(``); - assertSelector(doc, "html > body > .app > .layout > .page"); - assertTextMany(doc, "p", ["Layout: it works", "Page: it works"]); - }, - ); -}); diff --git a/tests/signal_test.ts b/tests/signal_test.ts deleted file mode 100644 index 46aa3402d2d..00000000000 --- a/tests/signal_test.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { waitForText, withPageName } from "./test_utils.ts"; - -Deno.test({ - name: "serializes shared signal references", - async fn() { - await withPageName("./tests/fixture/main.ts", async (page, address) => { - await page.goto(`${address}/signal_shared`); - await page.waitForSelector("#counter-1"); - - await page.click("#b-counter-1"); - await waitForText(page, "#counter-1 p", "2"); - await waitForText(page, "#counter-2 p", "2"); - - await page.click("#b-counter-2"); - await waitForText(page, "#counter-1 p", "3"); - await waitForText(page, "#counter-2 p", "3"); - }); - }, -}); diff --git a/tests/static_test.ts b/tests/static_test.ts deleted file mode 100644 index 2c69700aa18..00000000000 --- a/tests/static_test.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { assertEquals, dirname, fromFileUrl, join } from "./deps.ts"; -import { withFakeServe } from "./test_utils.ts"; - -Deno.test("don't fallthrough to /_fresh/static in dev", async () => { - const fixtureDir = join( - dirname(fromFileUrl(import.meta.url)), - "fixture_static", - ); - - try { - await Deno.mkdir(join(fixtureDir, "_fresh", "static"), { recursive: true }); - } catch (_err) { - // ignore - } - await Deno.writeTextFile( - join(fixtureDir, "_fresh", "static", "style.css"), - "h1 { color: blue; }", - ); - - await withFakeServe( - "./tests/fixture_static/dev.ts", - async (server) => { - const res = await server.get(`/style.css`); - const css = await res.text(); - assertEquals(css.replace(/\s+/g, ""), "h1{color:red;}"); - }, - ); - - await Deno.remove(join(fixtureDir, "_fresh", "static", "style.css")); - - await withFakeServe( - "./tests/fixture_static/dev.ts", - async (server) => { - const res = await server.get(`/style.css`); - const css = await res.text(); - assertEquals(css.replace(/\s+/g, ""), "h1{color:red;}"); - }, - ); -}); - -Deno.test("fallthrough to /_fresh/static in normal mode", async () => { - const fixtureDir = join( - dirname(fromFileUrl(import.meta.url)), - "fixture_static", - ); - - try { - await Deno.mkdir(join(fixtureDir, "_fresh", "static"), { recursive: true }); - } catch (_err) { - // ignore - } - await Deno.writeTextFile( - join(fixtureDir, "_fresh", "static", "style.css"), - "h1 { color: blue; }", - ); - - await withFakeServe( - "./tests/fixture_static/main.ts", - async (server) => { - const res = await server.get(`/style.css`); - const css = await res.text(); - assertEquals(css.replace(/\s+/g, ""), "h1{color:blue;}"); - }, - ); -}); diff --git a/tests/tailwind_test.ts b/tests/tailwind_test.ts deleted file mode 100644 index ad7b785102a..00000000000 --- a/tests/tailwind_test.ts +++ /dev/null @@ -1,124 +0,0 @@ -import { fetchHtml, runBuild, withFakeServe, withFresh } from "./test_utils.ts"; -import { - assert, - assertStringIncludes, - dirname, - join, - TextLineStream, -} from "./deps.ts"; -import { assertEquals } from "$std/assert/assert_equals.ts"; - -Deno.test("TailwindCSS - dev mode", async () => { - await withFakeServe("./tests/fixture_tailwind/dev.ts", async (server) => { - const res = await server.get("/styles.css"); - const content = await res.text(); - assertStringIncludes(content, ".text-red-600"); - - const res2 = await server.get("/styles.css?foo=bar"); - const content2 = await res2.text(); - assert(!content2.includes("@tailwind")); - }, { loadConfig: true }); -}); - -Deno.test("TailwindCSS - build mode", async () => { - await runBuild("./tests/fixture_tailwind_build/dev.ts"); - await withFakeServe( - "./tests/fixture_tailwind_build/main.ts", - async (server) => { - const res = await server.get("/styles.css"); - const content = await res.text(); - assertStringIncludes(content, ".text-red-600{"); - }, - { loadConfig: true }, - ); -}); - -Deno.test("TailwindCSS - build mode in sub directory", async () => { - await runBuild("./tests/fixture_tailwind_build_2/dev.ts"); - await withFakeServe( - "./tests/fixture_tailwind_build_2/main.ts", - async (server) => { - const res = await server.get("/foo/styles.css"); - const content = await res.text(); - assertStringIncludes(content, ".text-red-600{"); - }, - { loadConfig: true }, - ); -}); - -Deno.test("TailwindCSS - config", async () => { - await withFakeServe( - "./tests/fixture_tailwind_config/dev.ts", - async (server) => { - const res = await server.get("/styles.css"); - const content = await res.text(); - assertStringIncludes(content, ".text-pp"); - }, - { loadConfig: true }, - ); -}); - -Deno.test("TailwindCSS - middleware only css", async () => { - await withFakeServe( - "./tests/fixture_tailwind/dev.ts", - async (server) => { - const res = await server.get("/middleware-only.css"); - const content = await res.text(); - assertStringIncludes(content, ".foo-bar"); - }, - { loadConfig: true }, - ); -}); - -Deno.test("TailwindCSS - missing snapshot warning", async () => { - const dir = dirname(import.meta.url); - const out = await new Deno.Command(Deno.execPath(), { - args: ["run", "-A", join(dir, "./fixture_tailwind/main.ts")], - stdout: "piped", - stderr: "piped", - }).spawn(); - - const lines: ReadableStream = out.stderr - .pipeThrough(new TextDecoderStream()) - .pipeThrough(new TextLineStream()); - - let found = false; - // @ts-ignore yes it does - for await (const line of lines.values({ preventCancel: true })) { - if (!found && line.includes("No pre-compiled tailwind styles found")) { - found = true; - break; - } - } - - try { - assert(found, "Tailwind compile warning was not logged"); - } finally { - await out.stdout.cancel(); - out.kill("SIGTERM"); - await out.status; - - // Drain the lines stream - for await (const _ of lines) { /* noop */ } - } -}); - -Deno.test("TailwindCSS - missing snapshot on Deno Deploy", async () => { - await withFresh( - { - name: "./tests/fixture_tailwind/main.ts", - options: { - env: { - DENO_DEPLOYMENT_ID: "foo", - }, - }, - }, - async (address) => { - const doc = await fetchHtml(address); - assertEquals( - doc.querySelector("h1")?.textContent, - "Finish setting up Fresh", - ); - }, - ); -}); diff --git a/tests/test_utils.ts b/tests/test_utils.ts deleted file mode 100644 index fe93f1f43b8..00000000000 --- a/tests/test_utils.ts +++ /dev/null @@ -1,663 +0,0 @@ -import { - FromManifestConfig, - Manifest, - ServeHandlerInfo, - ServerContext, -} from "../server.ts"; -import { - assert, - assertEquals, - basename, - colors, - delay, - dirname, - DOMParser, - HTMLElement, - HTMLMetaElement, - join, - Page, - puppeteer, - TextLineStream, - toFileUrl, -} from "./deps.ts"; - -export interface TestDocument extends Document { - debug(): void; -} - -export function parseHtml(input: string): TestDocument { - // deno-lint-ignore no-explicit-any - const doc = new DOMParser().parseFromString(input, "text/html") as any; - Object.defineProperty(doc, "debug", { - value: () => console.log(prettyDom(doc)), - enumerable: false, - }); - return doc; -} - -export async function startFreshServer(options: Deno.CommandOptions) { - const { serverProcess, lines, address, output } = await spawnServer(options); - - if (!address) { - throw new Error("Server didn't start up"); - } - - return { serverProcess, lines, address, output }; -} - -export async function fetchHtml(url: string) { - const res = await fetch(url); - const html = await res.text(); - // deno-lint-ignore no-explicit-any - return new DOMParser().parseFromString(html, "text/html") as any as Document; -} - -export function assertSelector(doc: Document, selector: string) { - if (doc.querySelector(selector) === null) { - const html = prettyDom(doc); - throw new Error( - `Selector "${selector}" not found in document.\n\n${html}`, - ); - } -} - -export function assertNotSelector(doc: Document, selector: string) { - if (doc.querySelector(selector) !== null) { - const html = prettyDom(doc); - throw new Error( - `Selector "${selector}" found in document.\n\n${html}`, - ); - } -} - -export function assertTextMany( - doc: Document, - selector: string, - expected: string[], -) { - const texts = Array.from(doc.querySelectorAll(selector)).map((el) => - el.textContent - ); - - try { - assertEquals(texts, expected); - } catch (err) { - const html = "\n\n" + prettyDom(doc); - throw new err.constructor(err.message += html, { cause: err }); - } -} - -export function assertTextMatch( - doc: Document, - selector: string, - regex: RegExp, -) { - const texts = Array.from(doc.querySelectorAll(selector)).map((el) => - el.textContent - ).filter(Boolean) as string[]; - - if (!texts.some((text) => regex.test(text))) { - const html = "\n\n" + prettyDom(doc); - throw new Error( - `Regex ${regex} did not match any text elements in HTML.\n\n${html}`, - ); - } -} - -export const VOID_ELEMENTS = - /^(?:area|base|br|col|embed|hr|img|input|link|meta|param|source|track|wbr)$/; -function prettyDom(doc: Document) { - let out = colors.dim(`\n`); - - const node = doc.documentElement; - out += _printDomNode(node, 0); - - return out; -} - -function _printDomNode( - node: HTMLElement | Text | Node, - indent: number, -) { - const space = " ".repeat(indent); - - if (node.nodeType === 3) { - return space + colors.dim(node.textContent ?? "") + "\n"; - } else if (node.nodeType === 8) { - return space + colors.dim(`<--${(node as Text).data}-->`) + "\n"; - } - - let out = space; - if (node instanceof HTMLElement || node instanceof HTMLMetaElement) { - out += colors.dim(colors.cyan("<")); - out += colors.cyan(node.localName); - - for (let i = 0; i < node.attributes.length; i++) { - const attr = node.attributes.item(i); - if (attr === null) continue; - out += " " + colors.yellow(attr.name); - out += colors.dim("="); - out += colors.green(`"${attr.value}"`); - } - - if (VOID_ELEMENTS.test(node.localName)) { - out += colors.dim(colors.cyan(">")) + "\n"; - return out; - } - - out += colors.dim(colors.cyan(">")); - if (node.childNodes.length) { - out += "\n"; - - for (let i = 0; i < node.childNodes.length; i++) { - const child = node.childNodes[i]; - out += _printDomNode(child, indent + 1); - } - - out += space; - } - - out += colors.dim(colors.cyan("")); - out += "\n"; - } - - return out; -} - -export async function getErrorOverlay( - server: FakeServer, - url: string, -): Promise<{ title: string; codeFrame: boolean; stack: string }> { - const doc = await server.getHtml(url); - const iframe = doc.querySelector( - "#fresh-error-overlay", - ); - assert(iframe, "Missing fresh error overlay"); - - const doc2 = await server.getHtml(iframe.src); - - return { - title: doc2.querySelector(".title")!.textContent!, - codeFrame: doc2.querySelector(".code-frame") !== null, - stack: doc2.querySelector(".stack")!.textContent!, - }; -} - -export async function withFresh( - name: string | { name: string; options: Omit }, - fn: (address: string) => Promise, -) { - let file: string; - let options = {}; - - if (typeof name === "object") { - file = name.name; - options = name.options ?? {}; - } else { - file = name; - } - - const { lines, serverProcess, address } = await startFreshServer({ - ...options, - args: ["run", "-A", file], - }); - - try { - await fn(address); - } finally { - serverProcess.kill("SIGTERM"); - - // Wait until the process exits - await serverProcess.status; - - // Drain the lines stream - for await (const _ of lines) { /* noop */ } - } -} - -export async function withPageName( - name: string | { name: string; options: Omit }, - fn: (page: Page, address: string) => Promise, -) { - let file: string; - let options = {}; - - if (typeof name === "object") { - file = name.name; - options = name.options ?? {}; - } else { - file = name; - } - - const { lines, serverProcess, address } = await startFreshServer({ - ...options, - args: ["run", "-A", file], - }); - - try { - const browser = await puppeteer.launch({ args: ["--no-sandbox"] }); - - try { - const page = await browser.newPage(); - await fn(page, address); - } finally { - await browser.close(); - } - } finally { - serverProcess.kill("SIGTERM"); - // Wait until the process exits - await serverProcess.status; - - // Drain the lines stream - for await (const _ of lines) { /* noop */ } - } -} - -export interface FakeServer { - request(req: Request): Promise; - getHtml(pathname: string): Promise; - get(pathname: string): Promise; -} - -async function handleRequest( - handler: ReturnType, - conn: ServeHandlerInfo, - req: Request, -) { - let res = await handler(req, conn); - - // Follow redirects - while (res.headers.has("location")) { - let loc = res.headers.get("location")!; - const hostname = conn.remoteAddr.hostname; - if (!loc.startsWith("http://") && !loc.startsWith("https://")) { - loc = `https://${hostname}${loc}`; - } - - res = await handler(new Request(loc), conn); - } - - return res; -} - -export async function fakeServe( - manifest: Manifest, - config: FromManifestConfig, -): Promise { - const ctx = await ServerContext.fromManifest(manifest, config); - const handler = ctx.handler(); - - // deno-lint-ignore no-explicit-any - const conn: any = { - remoteAddr: { - transport: "tcp", - hostname: "127.0.0.1", - port: 80, - }, - }; - - const origin = `https://127.0.0.1`; - - return { - request(req) { - return handler(req, conn); - }, - async getHtml(pathname) { - const req = new Request(`${origin}${pathname}`); - const res = await handleRequest(handler, conn, req); - return parseHtml(await res.text()); - }, - get(pathname: string) { - const req = new Request(`${origin}${pathname}`); - return handleRequest(handler, conn, req); - }, - }; -} - -export async function withFakeServe( - name: string, - cb: (server: FakeServer) => Promise | void, - options: { loadConfig?: boolean } = {}, -) { - const fixture = join(Deno.cwd(), name); - const dev = basename(name) === "dev.ts"; - if (dev) { - try { - await Deno.remove(join(fixture, "_fresh")); - } catch (_err) { - // ignore - } - } - - const manifestPath = toFileUrl(join(dirname(fixture), "fresh.gen.ts")).href; - const manifestMod = await import(manifestPath); - - const configPath = join(dirname(fixture), "fresh.config.ts"); - - let config: FromManifestConfig = { dev }; - - // For now we load config on a case by case basis, because something in - // twind (unsure) doesn't work well if multiple instances are running - if (options.loadConfig) { - try { - const stats = await Deno.stat(configPath); - if (stats.isFile) { - const m = await import(toFileUrl(configPath).href); - config = m.default; - config.dev = dev; - } - } catch { - // ignore - } - } - - const server = await fakeServe(manifestMod.default, config); - await cb(server); -} - -export async function startFreshServerExpectErrors( - options: Deno.CommandOptions, -) { - const { serverProcess, lines, address } = await spawnServer(options, true); - - if (address) { - throw Error("Server started correctly"); - } - - const errorDecoder = new TextDecoderStream(); - const errorLines: ReadableStream = serverProcess.stderr - .pipeThrough(errorDecoder) - .pipeThrough(new TextLineStream(), { - preventCancel: true, - }); - let output = ""; - for await (const line of errorLines) { - output += line + "\n"; - } - - try { - serverProcess.kill("SIGTERM"); - } catch { - // ignore the error, this may throw on windows if the process has already - // exited - } - await serverProcess.status; - for await (const _ of lines) { /* noop */ } - - return output; -} - -/** - * Click on an element once it has an attached click listener - */ -export async function clickWhenListenerReady(page: Page, selector: string) { - await page.waitForSelector(selector); - await page.waitForFunction( - (sel) => { - const el = document.querySelector(sel)!; - - // Wait for Preact to have attached either a captured or non-captured - // click event - // deno-lint-ignore no-explicit-any - const preactListener = (el as any).l as Record | null; - if ( - !preactListener || typeof preactListener !== "object" || - (!preactListener.clickfalse && !preactListener.clicktrue) - ) { - return false; - } - - return true; - }, - {}, - selector, - ); - await page.click(selector); -} - -export async function waitForText( - page: Page, - selector: string, - text: string, -) { - await page.waitForSelector(selector); - try { - await page.waitForFunction( - (sel, value) => { - return document.querySelector(sel)!.textContent === value; - }, - { timeout: 2000 }, - selector, - String(text), - ); - } catch (err) { - const body = await page.content(); - // deno-lint-ignore no-explicit-any - const pretty = prettyDom(parseHtml(body) as any); - - console.log( - `Text "${text}" not found on selector "${selector}" in html:\n\n${pretty}`, - ); - throw err; - } -} - -export async function waitForStyle( - page: Page, - selector: string, - name: keyof CSSStyleDeclaration, - value: string, -) { - await page.waitForSelector(selector); - - const start = Date.now(); - let now = start; - let found = false; - while (now < start + 2000) { - found = await page.evaluate( - (s, n, v) => { - const el = document.querySelector(s); - if (!el) return false; - return globalThis.getComputedStyle(el)[n] === v; - }, - selector, - name, - value, - ); - - if (found) break; - - await delay(200); - now = Date.now(); - } - - if (!found) { - console.log(prettyDom(parseHtml(await page.content()))); - throw new Error(`Could not find style ${String(name)}: ${value}`); - } -} - -async function spawnServer( - options: Deno.CommandOptions, - expectErrors = false, -) { - const serverProcess = new Deno.Command(Deno.execPath(), { - ...options, - stdin: "null", - stdout: "piped", - stderr: expectErrors ? "piped" : "inherit", - }).spawn(); - - const lines: ReadableStream = serverProcess.stdout - .pipeThrough(new TextDecoderStream()) - .pipeThrough(new TextLineStream()); - - const output: string[] = []; - let address = ""; - // @ts-ignore yes it does - for await (const line of lines.values({ preventCancel: true })) { - output.push(line); - const match = line.match( - /https?:\/\/localhost:\d+(\/\w+[-\w]*)*/g, - ); - if (match) { - address = match[0]; - break; - } - } - - return { serverProcess, lines, address, output }; -} - -export async function recreateFolder(folderPath: string) { - try { - await Deno.remove(folderPath); - } catch { - // ignore - } - try { - await Deno.mkdir(folderPath, { recursive: true }); - } catch { - // ignore - } -} - -export async function runBuild(fixture: string) { - const outDir = join(dirname(fixture), "_fresh"); - try { - await Deno.remove(outDir, { recursive: true }); - } catch { - // Ignore - } - - assert( - fixture.endsWith("dev.ts"), - `Build command only works with "dev.ts", but got "${fixture}" instead`, - ); - const res = await new Deno.Command(Deno.execPath(), { - args: [ - "run", - "-A", - fixture, - "build", - ], - env: { - GITHUB_SHA: "__BUILD_ID__", - DENO_DEPLOYMENT_ID: "__BUILD_ID__", - }, - stdin: "null", - stdout: "piped", - stderr: "piped", - }).output(); - - const output = getStdOutput(res); - return { - code: res.code, - stderr: output.stderr, - stdout: output.stdout, - }; -} - -export function getStdOutput( - out: Deno.CommandOutput, -): { stdout: string; stderr: string } { - const decoder = new TextDecoder(); - const stdout = colors.stripColor(decoder.decode(out.stdout)); - - const decoderErr = new TextDecoder(); - const stderr = colors.stripColor(decoderErr.decode(out.stderr)); - - return { stdout, stderr }; -} - -export async function waitFor( - fn: () => Promise | unknown, -): Promise { - let now = Date.now(); - const limit = now + 2000; - - while (now < limit) { - try { - if (await fn()) return; - } catch (err) { - if (now > limit) { - throw err; - } - } finally { - await delay(100); - now = Date.now(); - } - } - - throw new Error(`Timed out`); -} - -function walk(doc: Document, node: HTMLElement): string | null { - for (let i = 0; i < node.childNodes.length; i++) { - const child = node.childNodes[i]; - - if (child.nodeType === doc.COMMENT_NODE) { - return child.data; - } else if (child.nodeType === doc.TEXT_NODE) { - continue; - } else if ( - child.nodeType === doc.ELEMENT_NODE && node.localName !== "template" - ) { - const res = walk(doc, child); - if (res !== null) return res; - } - } - return null; -} - -export async function assertNoPageComments(page: Page) { - const doc = parseHtml(await page.content()); - - // deno-lint-ignore no-explicit-any - const result = walk(doc, doc.body as any); - - if (result !== null) { - console.log(prettyDom(doc)); - throw new Error( - `Expected no HTML comments to be present, but found comment "${result}"`, - ); - } -} - -export function assertNoComments(doc: Document) { - // deno-lint-ignore no-explicit-any - const result = walk(doc, doc.body as any); - - if (result !== null) { - console.log(prettyDom(doc)); - throw new Error( - `Expected no HTML comments to be present, but found comment "${result}"`, - ); - } -} - -export function assertMetaContent( - doc: Document, - nameOrProperty: string, - expected: string, -) { - let el = doc.querySelector(`meta[name="${nameOrProperty}"]`) as - | HTMLMetaElement - | null; - - if (el === null) { - el = doc.querySelector(`meta[property="${nameOrProperty}"]`) as - | HTMLMetaElement - | null; - } - - if (el === null) { - console.log(prettyDom(doc)); - throw new Error( - `No -tag found with content "${expected}"`, - ); - } - assertEquals(el.content, expected); -} diff --git a/tests/test_utils.tsx b/tests/test_utils.tsx new file mode 100644 index 00000000000..738afefe855 --- /dev/null +++ b/tests/test_utils.tsx @@ -0,0 +1,383 @@ +import { App, setBuildCache } from "../src/app.ts"; +import { launch, type Page } from "@astral/astral"; +import * as colors from "@std/fmt/colors"; +import { type Document, DOMParser, HTMLElement } from "linkedom"; +import { Builder } from "../src/dev/builder.ts"; +import { TextLineStream } from "@std/streams/text-line-stream"; +import * as path from "@std/path"; +import type { ComponentChildren } from "preact"; +import { expect } from "@std/expect"; +import { ProdBuildCache } from "../src/build_cache.ts"; +import { Counter } from "./fixtures_islands/Counter.tsx"; +import { CounterWithSlots } from "./fixtures_islands/CounterWithSlots.tsx"; +import { EscapeIsland } from "./fixtures_islands/EscapeIsland.tsx"; +import { FnIsland } from "./fixtures_islands/FnIsland.tsx"; +import { FragmentIsland } from "./fixtures_islands/FragmentIsland.tsx"; +import { IslandInIsland } from "./fixtures_islands/IslandInIsland.tsx"; +import { JsonIsland } from "./fixtures_islands/JsonIsland.tsx"; +import { JsxChildrenIsland } from "./fixtures_islands/JsxChildrenIsland.tsx"; +import { JsxConditional } from "./fixtures_islands/JsxConditional.tsx"; +import { JsxIsland } from "./fixtures_islands/JsxIsland.tsx"; +import { NullIsland } from "./fixtures_islands/NullIsland.tsx"; +import { PartialInIsland } from "./fixtures_islands/PartialInIsland.tsx"; +import { PassThrough } from "./fixtures_islands/PassThrough.tsx"; +import { SelfCounter } from "./fixtures_islands/SelfCounter.tsx"; +import { Multiple1, Multiple2 } from "./fixtures_islands/Multiple.tsx"; +import { Foo } from "./fixture_island_groups/routes/foo/(_islands)/Foo.tsx"; + +export function getIsland(pathname: string) { + return path.join( + import.meta.dirname!, + "fixtures_islands", + pathname, + ); +} + +export const charset = ; + +export const favicon = ( + +); + +export function Doc(props: { children?: ComponentChildren; title?: string }) { + return ( + + + {charset} + {props.title ?? "Test"} + {favicon} + + + {props.children} + + + ); +} + +export async function buildProd(app: App) { + const outDir = await Deno.makeTempDir(); + // FIXME: Sharing build output path is weird + app.config.build.outDir = outDir; + const builder = new Builder({}); + await builder.build(app); + const cache = await ProdBuildCache.fromSnapshot(app.config); + setBuildCache(app, cache); +} + +export async function withBrowserApp( + app: App, + fn: (page: Page, address: string) => void | Promise, +) { + const aborter = new AbortController(); + let server: Deno.HttpServer | null = null; + let port = 0; + try { + server = await Deno.serve({ + hostname: "localhost", + port: 0, + signal: aborter.signal, + onListen: ({ port: p }) => { + port = p; + }, + }, await app.handler()); + + const browser = await launch({ + args: ["--no-sandbox"], + // headless: false, + }); + + const page = await browser.newPage(); + try { + await fn(page, `http://localhost:${port}`); + } finally { + await page.close(); + await browser.close(); + } + } finally { + aborter.abort(); + await server?.finished; + } +} + +export async function withBrowser(fn: (page: Page) => void | Promise) { + const aborter = new AbortController(); + try { + const browser = await launch({ + args: ["--no-sandbox"], + // headless: false, + }); + + const page = await browser.newPage(); + // page.setDefaultTimeout(1000000); + try { + await fn(page); + } finally { + await page.close(); + await browser.close(); + } + } finally { + aborter.abort(); + } +} + +export async function withChildProcessServer( + dir: string, + entry: string, + fn: (address: string) => void | Promise, +) { + const aborter = new AbortController(); + const cp = await new Deno.Command(Deno.execPath(), { + args: ["run", "-A", entry], + stdin: "null", + stdout: "piped", + stderr: "inherit", + cwd: dir, + signal: aborter.signal, + }).spawn(); + + const lines: ReadableStream = cp.stdout + .pipeThrough(new TextDecoderStream()) + .pipeThrough(new TextLineStream()); + + const output: string[] = []; + let address = ""; + let found = false; + // @ts-ignore yes it does + for await (const line of lines.values({ preventCancel: true })) { + output.push(line); + const match = line.match( + /https?:\/\/localhost:\d+(\/\w+[-\w]*)*/g, + ); + if (match) { + address = match[0]; + found = true; + break; + } + } + + if (!found) { + throw new Error(`Could not find server addrews`); + } + + try { + await fn(address); + } finally { + aborter.abort(); + await cp.status; + for await (const _ of lines) { /* noop */ } + } +} + +export const VOID_ELEMENTS = + /^(?:area|base|br|col|embed|hr|img|input|link|meta|param|source|track|wbr)$/; +function prettyDom(doc: Document) { + let out = colors.dim(`\n`); + + const node = doc.documentElement; + out += _printDomNode(node, 0); + + return out; +} + +function _printDomNode( + node: HTMLElement | Text | Node, + indent: number, +) { + const space = " ".repeat(indent); + + if (node.nodeType === 3) { + return space + colors.dim(node.textContent ?? "") + "\n"; + } else if (node.nodeType === 8) { + return space + colors.dim(`<--${(node as Text).data}-->`) + "\n"; + } + + let out = space; + if (node instanceof HTMLElement || node instanceof HTMLMetaElement) { + out += colors.dim(colors.cyan("<")); + out += colors.cyan(node.localName); + + for (let i = 0; i < node.attributes.length; i++) { + const attr = node.attributes.item(i); + if (attr === null) continue; + out += " " + colors.yellow(attr.name); + out += colors.dim("="); + out += colors.green(`"${attr.value}"`); + } + + if (VOID_ELEMENTS.test(node.localName)) { + out += colors.dim(colors.cyan(">")) + "\n"; + return out; + } + + out += colors.dim(colors.cyan(">")); + if (node.childNodes.length) { + out += "\n"; + + for (let i = 0; i < node.childNodes.length; i++) { + const child = node.childNodes[i]; + out += _printDomNode(child, indent + 1); + } + + out += space; + } + + out += colors.dim(colors.cyan("")); + out += "\n"; + } + + return out; +} + +export interface TestDocument extends Document { + debug(): void; +} + +export function parseHtml(input: string): TestDocument { + // deno-lint-ignore no-explicit-any + const doc = new DOMParser().parseFromString(input, "text/html") as any; + Object.defineProperty(doc, "debug", { + value: () => console.log(prettyDom(doc)), + enumerable: false, + }); + return doc; +} + +export function assertSelector(doc: Document, selector: string) { + if (doc.querySelector(selector) === null) { + const html = prettyDom(doc); + throw new Error( + `Selector "${selector}" not found in document.\n\n${html}`, + ); + } +} + +export function assertNotSelector(doc: Document, selector: string) { + if (doc.querySelector(selector) !== null) { + const html = prettyDom(doc); + throw new Error( + `Selector "${selector}" found in document.\n\n${html}`, + ); + } +} + +export function assertMetaContent( + doc: Document, + nameOrProperty: string, + expected: string, +) { + let el = doc.querySelector(`meta[name="${nameOrProperty}"]`) as + | HTMLMetaElement + | null; + + if (el === null) { + el = doc.querySelector(`meta[property="${nameOrProperty}"]`) as + | HTMLMetaElement + | null; + } + + if (el === null) { + console.log(prettyDom(doc)); + throw new Error( + `No -tag found with content "${expected}"`, + ); + } + expect(el.content).toEqual(expected); +} + +export async function waitForText( + page: Page, + selector: string, + text: string, +) { + await page.waitForSelector(selector); + try { + await page.waitForFunction( + (sel: string, value: string) => { + const el = document.querySelector(sel); + if (el === null) return false; + return el.textContent === value; + }, + { args: [selector, String(text)] }, + ); + } catch (err) { + const body = await page.content(); + // deno-lint-ignore no-explicit-any + const pretty = prettyDom(parseHtml(body) as any); + + console.log( + `Text "${text}" not found on selector "${selector}" in html:\n\n${pretty}`, + ); + throw err; + } +} + +export async function waitFor( + fn: () => Promise | unknown, +): Promise { + let now = Date.now(); + const limit = now + 2000; + + while (now < limit) { + try { + if (await fn()) return; + } catch (err) { + if (now > limit) { + throw err; + } + } finally { + await new Promise((r) => setTimeout(r, 250)); + now = Date.now(); + } + } + + throw new Error(`Timed out`); +} + +export function getStdOutput( + out: Deno.CommandOutput, +): { stdout: string; stderr: string } { + const decoder = new TextDecoder(); + const stdout = colors.stripColor(decoder.decode(out.stdout)); + + const decoderErr = new TextDecoder(); + const stderr = colors.stripColor(decoderErr.decode(out.stderr)); + + return { stdout, stderr }; +} + +export const allIslandApp = new App() + .island(getIsland("Counter.tsx"), "Counter", Counter) + .island( + getIsland("CounterWithSlots.tsx"), + "CounterWithSlots", + CounterWithSlots, + ) + .island(getIsland("EscapeIsland.tsx"), "EscapeIsland", EscapeIsland) + .island(getIsland("FnIsland.tsx"), "FnIsland", FnIsland) + .island(getIsland("FragmentIsland.tsx"), "FragmentIsland", FragmentIsland) + .island(getIsland("IslandInIsland.tsx"), "IslandInIsland", IslandInIsland) + .island(getIsland("JsonIsland.tsx"), "JsonIsland", JsonIsland) + .island( + getIsland("JsxChildrenIsland.tsx"), + "JsxChildrenIsland", + JsxChildrenIsland, + ) + .island(getIsland("JsxConditional.tsx"), "JsxConditional", JsxConditional) + .island(getIsland("JsxIsland.tsx"), "JsxIsland", JsxIsland) + .island(getIsland("Multiple.tsx"), "Multiple1", Multiple1) + .island(getIsland("Multiple.tsx"), "Multiple2", Multiple2) + .island(getIsland("NullIsland.tsx"), "NullIsland", NullIsland) + .island(getIsland("PartialInIsland.tsx"), "PartialInIsland", PartialInIsland) + .island(getIsland("PassThrough.tsx"), "PassThrough", PassThrough) + .island(getIsland("SelfCounter.tsx"), "SelfCounter", SelfCounter) + .island( + getIsland("../fixture_island_groups/routes/foo/(_islands)/Foo.tsx"), + "Foo", + Foo, + ); diff --git a/tests/trailing_slash_test.ts b/tests/trailing_slash_test.ts deleted file mode 100644 index 55e45c243af..00000000000 --- a/tests/trailing_slash_test.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { INTERNAL_PREFIX } from "../runtime.ts"; -import { ServerContext, STATUS_CODE } from "../server.ts"; -import { assert, assertEquals } from "./deps.ts"; -import manifest from "./fixture_router/fresh.gen.ts"; - -const ctx = await ServerContext.fromManifest(manifest, { - router: { - trailingSlash: true, - }, -}); -const router = (req: Request) => { - return ctx.handler()(req, { - remoteAddr: { - transport: "tcp", - hostname: "127.0.0.1", - port: 80, - }, - // deno-lint-ignore no-explicit-any - } as any); -}; - -Deno.test("forwards slash placed at the end of url", async () => { - const targetUrl = "https://fresh.deno.dev/about"; - const resp = await router(new Request(targetUrl)); - assert(resp); - assertEquals(resp.status, STATUS_CODE.PermanentRedirect); - // forwarded location should be with trailing slash - assertEquals(resp.headers.get("location"), targetUrl + "/"); -}); - -Deno.test("forwards slash placed at the end of url with hash and query string", async () => { - const targetUrl = "https://fresh.deno.dev/about"; - const queryAndHash = "?demo=test#what"; - const resp = await router(new Request(targetUrl + queryAndHash)); - assert(resp); - assertEquals(resp.status, STATUS_CODE.PermanentRedirect); - // forwarded location should be with trailing slash - assertEquals(resp.headers.get("location"), targetUrl + "/" + queryAndHash); -}); - -Deno.test("forwards slash not placed at the end of url with prefix", async () => { - const targetUrl = `https://fresh.deno.dev${INTERNAL_PREFIX}/no_redirect`; - const resp = await router(new Request(targetUrl)); - assert(resp); - // we should get a 404 and not a redirect - assertEquals(resp.status, STATUS_CODE.NotFound); -}); - -Deno.test("forwards slash not placed at the end of url for static file", async () => { - const targetUrl = `https://fresh.deno.dev/foo.txt`; - const resp = await router(new Request(targetUrl)); - assert(resp); - // we should not be getting a redirect - assertEquals(resp.status, STATUS_CODE.OK); - assertEquals(await resp.text(), "bar"); -}); diff --git a/tests/twind_test.ts b/tests/twind_test.ts deleted file mode 100644 index f425044de12..00000000000 --- a/tests/twind_test.ts +++ /dev/null @@ -1,407 +0,0 @@ -import { assert, assertEquals, assertMatch, delay, puppeteer } from "./deps.ts"; - -import { cmpStringArray } from "./fixture_twind_hydrate/utils/utils.ts"; -import { - startFreshServer, - waitForStyle, - withFakeServe, - withFresh, - withPageName, -} from "./test_utils.ts"; - -/** - * Start the server with the main file. - * - * Returns a page instance and a method to terminate the server. - */ -async function setUpServer(path: string) { - const { lines, serverProcess, address } = await startFreshServer({ - args: ["run", "-A", path], - }); - - await delay(100); - - const browser = await puppeteer.launch({ args: ["--no-sandbox"] }); - const page = await browser.newPage(); - - /** - * terminate server - */ - const terminate = async () => { - await browser.close(); - - serverProcess.kill("SIGKILL"); - await serverProcess.status; - - // Drain the lines stream - for await (const _ of lines) { /* noop */ } - }; - - return { page: page, terminate, address }; -} - -/** - * Main file path - */ -const MAIN_FILE_PATH = "./tests/fixture_twind_hydrate/main.ts"; - -Deno.test({ - name: "twind static test", - async fn(t) { - // Preparation - const server = await setUpServer(MAIN_FILE_PATH); - const page = server.page; - - /** - * Compare the class of element of any id with the selectorText of cssrules in stylesheet. - * Ensure that twind compiles the class of element. - */ - async function compiledCssRulesTest(id: string, styleId: string) { - const elemClassList = await page.evaluate((selector) => { - const classList = document.querySelector(selector)?.classList; - if (classList != null) { - return Array.from(classList); - } else { - return []; - } - }, `#${id}`); - assert(elemClassList.length !== 0, `Element of id=${id} has no class`); - - const twindCssRules = await page.evaluate((selector) => { - const styleElem = document.querySelector(selector); - if (styleElem == null) { - return []; - } - const cssRules = (styleElem as HTMLStyleElement).sheet?.cssRules; - if (cssRules == null) { - return []; - } - - return Array.from(cssRules).map((cssRule) => { - const cssStyleRule = cssRule as CSSStyleRule; - return cssStyleRule.selectorText; - }); - }, `#${styleId}`); - - const twindCssRulesSet = new Set(twindCssRules); - - for (const elemClass of elemClassList) { - assert( - twindCssRulesSet.has("." + elemClass), - `'${elemClass}' is not compiled by twind`, - ); - } - } - - await page.goto(`${server.address}/static`, { - waitUntil: "networkidle2", - }); - - // For avoid leaking async ops. - await delay(10); - - await t.step("Twind complies cssrules from dom class in ssr", async () => { - await compiledCssRulesTest("helloTwind", "__FRSH_TWIND"); - }); - - await server.terminate(); - }, -}); - -Deno.test({ - name: "No duplicate twind cssrules", - async fn(t) { - // Preparation - const server = await setUpServer(MAIN_FILE_PATH); - const page = server.page; - - /** - * Ensure that the cssrule of the two style elements specified do not duplicate. - * PR946 fails and PR1050 passes. - */ - async function noDuplicatesTest( - twindStyleElemSelector: string, - twindClaimedStyleElemSelector: string, - ) { - const twindCssRuleList = await page.evaluate((selector) => { - const elem = document.querySelector(selector) as HTMLStyleElement; - const cssRules = elem?.sheet?.cssRules; - if (cssRules != null) { - return Array.from(cssRules).map((el) => { - return el.cssText; - }); - } else { - return null; - } - }, twindStyleElemSelector); - - if (twindCssRuleList == null) { - assert(false, `StyleElement(${twindStyleElemSelector}) is no exists`); - } - - const twindClaimedCssRuleList = await page.evaluate((selector) => { - const elem = document.querySelector(selector) as HTMLStyleElement; - const cssRules = elem?.sheet?.cssRules; - if (cssRules != null) { - return Array.from(cssRules).map((el) => { - return el.cssText; - }); - } else { - return null; - } - }, twindClaimedStyleElemSelector); - - if (twindClaimedCssRuleList == null) { - // There is only one style element by twind. - return; - } - - const numDuplicates = cmpStringArray( - twindCssRuleList, - twindClaimedCssRuleList, - ); - - assert(false, `${numDuplicates} cssrules are duplicated`); - } - - await page.goto(`${server.address}/check-duplication`, { - waitUntil: "networkidle2", - }); - - // For avoid leaking async ops. - await delay(10); - - await t.step("Ensure no dupulicate twind cssrules in islands", async () => { - await noDuplicatesTest( - "#__FRSH_TWIND", - '[data-twind="claimed"]:not(#__FRSH_TWIND)', - ); - }); - - await server.terminate(); - }, -}); - -Deno.test({ - name: "Dynamically insert cssrules", - async fn(t) { - // Preparation - const server = await setUpServer(MAIN_FILE_PATH); - const page = server.page; - - /** - * Ensure that the class dynamically inserted in islands is compiled by twind. - * PR946 fails and PR1050 passes. - */ - async function DynamicallyInsertCssrulesTest(twindStyleId: string) { - const numCssRulesBeforeInsert = await page.$eval( - `#${twindStyleId}`, - (el) => { - const styleElem = el as HTMLStyleElement; - const cssRules = styleElem.sheet?.cssRules; - const numCssRules = cssRules?.length; - - return numCssRules != null ? numCssRules : NaN; - }, - ); - - assert( - !isNaN(numCssRulesBeforeInsert), - "StyleElement(#${twindStyleId}) is no exists", - ); - - const classBeforeInsert = await page.$eval( - "#currentNumCssRules", - (el) => { - return Array.from(el.classList) as string[]; - }, - ); - - // After click, `text-green-600` is inserted to the class of the element in #currentNumCssRules. - await page.$eval("#insertCssRuleButton", (el) => { - return el.click(); - }); - - const [numCssRulesAfterInsert, twindCssRulesAfterInsert] = await page - .$eval(`#${twindStyleId}`, (el) => { - const styleElem = el as HTMLStyleElement; - const cssRules = styleElem.sheet?.cssRules; - const numCssRules = cssRules?.length; - const cssRulesSelectorTextArray = cssRules != null - ? Array.from(cssRules).map((el) => { - return (el as CSSStyleRule).selectorText; - }) - : null; - - return [ - numCssRules != null ? numCssRules : NaN, - cssRulesSelectorTextArray, - ] as [number, string[] | null]; - }); - - assert( - !isNaN(numCssRulesAfterInsert), - `StyleElement(#${twindStyleId}) is no exists`, - ); - - const classAfterInsert = await page.$eval("#currentNumCssRules", (el) => { - return Array.from(el.classList) as string[]; - }); - - const classBeforeInsertSet = new Set(classBeforeInsert); - - const dynInsertedClassArray = classAfterInsert.filter((c) => { - return !classBeforeInsertSet.has(c); - }); - - // Check if the added class is compiled by twind. - const twindCssRulesAfterInsertSet = new Set(twindCssRulesAfterInsert); - for (const insertedClass of dynInsertedClassArray) { - assert( - twindCssRulesAfterInsertSet.has(`.${insertedClass}`), - `'${insertedClass} has been inserted into a style sheet other than