From 37823573a3931d4fdf08fbb607e9f56da6e24277 Mon Sep 17 00:00:00 2001 From: Mike White <64708423+mikehw@users.noreply.github.com> Date: Wed, 1 Nov 2023 20:47:22 -0500 Subject: [PATCH 1/4] Add importing islands without a file --- docs/canary/concepts/islands.md | 221 ++++++++++++++++++++++ src/dev/deps.ts | 1 + src/dev/dev_command.ts | 6 +- src/dev/mod.ts | 46 ++++- src/server/fs_extract.ts | 21 +- src/server/types.ts | 5 + tests/fixture_island_url/deno.json | 14 ++ tests/fixture_island_url/dev.ts | 10 + tests/fixture_island_url/fresh.gen.ts | 21 ++ tests/fixture_island_url/main.ts | 10 + tests/fixture_island_url/routes/index.tsx | 14 ++ tests/islands_test.ts | 23 +++ 12 files changed, 377 insertions(+), 15 deletions(-) create mode 100644 docs/canary/concepts/islands.md create mode 100644 tests/fixture_island_url/deno.json create mode 100755 tests/fixture_island_url/dev.ts create mode 100644 tests/fixture_island_url/fresh.gen.ts create mode 100644 tests/fixture_island_url/main.ts create mode 100644 tests/fixture_island_url/routes/index.tsx diff --git a/docs/canary/concepts/islands.md b/docs/canary/concepts/islands.md new file mode 100644 index 00000000000..0e3f0550cf4 --- /dev/null +++ b/docs/canary/concepts/islands.md @@ -0,0 +1,221 @@ +--- +description: | + Islands enable client side interactivity in Fresh. They are hydrated on the client in addition to being rendered on the server. +--- + +Islands enable client side interactivity in Fresh. Islands are isolated Preact +components that are rendered on the server and then hydrated on the client. This +is different from all other components in Fresh, as they are usually rendered on +the server only. + +Islands are defined by creating a file in the `islands/` folder in a Fresh +project. The name of this file must be a PascalCase or kebab-case name of the +island. + +```tsx islands/my-island.tsx +import { useSignal } from "@preact/signals"; + +export default function MyIsland() { + const count = useSignal(0); + + return ( +
+ Counter is at {count}.{" "} + +
+ ); +} +``` + +An island can be used in a page like a regular Preact component. Fresh will take +care of automatically re-hydrating the island on the client. + +```tsx route/index.tsx +import MyIsland from "../islands/my-island.tsx"; + +export default function Home() { + return ; +} +``` + +Islands may also be imported by URL. This is useful for using islands from +shared libraries. Island URLs must be listed in the FreshConfig. + +```ts dev.ts +await dev(import.meta.url, "./main.ts", { + islandUrls: [ + "https://deno.land/x/fresh@1.5.2/demo/islands/Counter.tsx", + ], +}); +``` + +```tsx route/index.tsx +import { useSignal } from "@preact/signals"; +import Counter from "https://deno.land/x/fresh@1.5.2/demo/islands/Counter.tsx"; + +export default function Home() { + const count = useSignal(3); + return ; +} +``` + +## Passing JSX to islands + +Islands support passing JSX elements via the `children` property. + +```tsx islands/my-island.tsx +import { useSignal } from "@preact/signals"; +import { ComponentChildren } from "preact"; + +interface Props { + children: ComponentChildren; +} + +export default function MyIsland({ children }: Props) { + const count = useSignal(0); + + return ( +
+ Counter is at {count}.{" "} + + {children} +
+ ); +} +``` + +This allows you to pass static content rendered by the server to an island in +the browser. + +```tsx routes/index.tsx +import MyIsland from "../islands/my-island.tsx"; + +export default function Home() { + return ( + +

This text is rendered on the server

+
+ ); +} +``` + +## Passing other props to islands + +Passing props to islands is supported, but only if the props are serializable. +Fresh can serialize the following types of values: + +- Primitive types `string`, `boolean`, `bigint`, and `null` +- Most `number`s (`Infinity`, `-Infinity`, and `NaN` are silently converted to + `null`) +- Plain objects with string keys and serializable values +- Arrays containing serializable values +- Uint8Array +- JSX Elements (restricted to `props.children`) +- Preact Signals (if the inner value is serializable) + +Circular references are supported. If an object or signal is referenced multiple +times, it is only serialized once and the references are restored upon +deserialization. Passing complex objects like `Date`, custom classes, or +functions is not supported. + +During server side rendering, Fresh annotates the HTML with special comments +that indicate where each island will go. This gives the code sent to the client +enough information to put the islands where they are supposed to go without +requiring hydration for the static children of interactive islands. No +Javascript is sent to the client when no interactivity is needed. + +```html + +
+ Counter is at 0. + + +

This text is rendered on the server

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

String from props: {foo}

+

+ + {" "} + number is: {number}. +

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

Some more server rendered text

+
+ ); +} +``` + +## Rendering islands on client only + +When using client-only APIs, like `EventSource` or `navigator.getUserMedia`, +this component will not run on the server as it will produce an error like: + +``` +An error occurred during route handling or page rendering. ReferenceError: EventSource is not defined + at Object.MyIsland (file:///Users/someuser/fresh-project/islandsmy-island.tsx:6:18) + at m (https://esm.sh/v129/preact-render-to-string@6.2.0/X-ZS8q/denonext/preact-render-to-string.mjs:2:2602) + at m (https://esm.sh/v129/preact-render-to-string@6.2.0/X-ZS8q/denonext/preact-render-to-string.mjs:2:2113) + .... +``` + +Use the [`IS_BROWSER`](https://deno.land/x/fresh/runtime.ts?doc=&s=IS_BROWSER) +flag as a guard to fix the issue: + +```tsx islands/my-island.tsx +import { IS_BROWSER } from "$fresh/runtime.ts"; + +export function MyIsland() { + // Return any prerenderable JSX here which makes sense for your island + if (!IS_BROWSER) return
; + + // All the code which must run in the browser comes here! + // Like: EventSource, navigator.getUserMedia, etc. + return
; +} +``` diff --git a/src/dev/deps.ts b/src/dev/deps.ts index 6ac6533f16e..4275509b75b 100644 --- a/src/dev/deps.ts +++ b/src/dev/deps.ts @@ -24,6 +24,7 @@ export { existsSync } from "https://deno.land/std@0.193.0/fs/mod.ts"; export * as semver from "https://deno.land/std@0.195.0/semver/mod.ts"; export * as JSONC from "https://deno.land/std@0.195.0/jsonc/mod.ts"; export * as fs from "https://deno.land/std@0.195.0/fs/mod.ts"; +export { isWindows } from "https://deno.land/std@0.195.0/_util/os.ts"; // ts-morph export { Node, Project } from "https://deno.land/x/ts_morph@17.0.1/mod.ts"; diff --git a/src/dev/dev_command.ts b/src/dev/dev_command.ts index 7d840b09123..0ee138dcbab 100644 --- a/src/dev/dev_command.ts +++ b/src/dev/dev_command.ts @@ -26,7 +26,11 @@ export async function dev( } else { currentManifest = { islands: [], routes: [] }; } - const newManifest = await collect(dir, config?.router?.ignoreFilePattern); + const newManifest = await collect( + dir, + config?.router?.ignoreFilePattern, + config?.islandUrls, + ); Deno.env.set("FRSH_DEV_PREVIOUS_MANIFEST", JSON.stringify(newManifest)); const manifestChanged = diff --git a/src/dev/mod.ts b/src/dev/mod.ts index 401ec4bd5fa..da56e97ddb9 100644 --- a/src/dev/mod.ts +++ b/src/dev/mod.ts @@ -1,4 +1,13 @@ -import { gte, join, posix, relative, walk, WalkEntry } from "./deps.ts"; +import { + gte, + isWindows, + join, + posix, + relative, + SEP, + walk, + WalkEntry, +} from "./deps.ts"; import { error } from "./error.ts"; const MIN_DENO_VERSION = "1.31.0"; const TEST_FILE_PATTERN = /[._]test\.(?:[tj]sx?|[mc][tj]s)$/; @@ -55,6 +64,7 @@ const GROUP_REG = /[/\\\\]\((_[^/\\\\]+)\)[/\\\\]/; export async function collect( directory: string, ignoreFilePattern?: RegExp, + islandUrls?: string[], ): Promise { const filePaths = new Set(); @@ -84,11 +94,13 @@ export async function collect( filePaths.add(normalized); routes.push(rel); }, ignoreFilePattern), - collectDir(join(directory, "./islands"), (entry, dir) => { - const rel = join("islands", relative(dir, entry.path)); - islands.push(rel); + collectDir(join(directory, "./islands"), (entry) => { + islands.push(getManifestItemFromPath(directory, entry.path)); }, ignoreFilePattern), ]); + if (islandUrls) { + islands.push(...islandUrls); + } routes.sort(); islands.sort(); @@ -96,10 +108,22 @@ export async function collect( return { routes, islands }; } +function isURL(url: unknown): boolean { + try { + new URL(url as string); + return true; + } catch { + return false; + } +} + /** * Import specifiers must have forward slashes */ function toImportSpecifier(file: string) { + if (isURL(file)) { + return file; + } let specifier = posix.normalize(file).replace(/\\/g, "/"); if (!specifier.startsWith(".")) { specifier = "./" + specifier; @@ -176,3 +200,17 @@ export default manifest; "color: blue; font-weight: bold", ); } + +function getManifestItemFromPath(directory: string, filePath: string) { + let relativePath = relative(directory, filePath); + + if (!relativePath.startsWith(".") && !relativePath.startsWith(SEP)) { + relativePath = `.${SEP}${relativePath}`; + } + + if (isWindows) { + relativePath = relativePath.replaceAll(SEP, posix.sep); + } + + return relativePath; +} diff --git a/src/server/fs_extract.ts b/src/server/fs_extract.ts index 3003c82a4b5..0fa45da107d 100644 --- a/src/server/fs_extract.ts +++ b/src/server/fs_extract.ts @@ -235,15 +235,13 @@ export async function extractRoutes( for (const [self, module] of Object.entries(manifest.islands)) { const url = new URL(self, baseUrl).href; - if (!url.startsWith(baseUrl)) { - throw new TypeError("Island is not a child of the basepath."); - } - let path = url.substring(baseUrl.length); - if (path.startsWith("islands")) { - path = path.slice("islands".length + 1); + let baseRoute = self.substring(0, self.length - extname(self).length); + if (self.startsWith("./islands/")) { + baseRoute = self.substring( + "./islands/".length, + self.length - extname(self).length, + ); } - const baseRoute = path.substring(0, path.length - extname(path).length); - for (const [exportName, exportedFunction] of Object.entries(module)) { if (typeof exportedFunction !== "function") { continue; @@ -475,8 +473,11 @@ function toPascalCase(text: string): string { } function sanitizeIslandName(name: string): string { - const fileName = name.replaceAll(/[/\\\\\(\)\[\]]/g, "_"); - return toPascalCase(fileName); + // Remove all non-alphanumeric characters to make a safe variable name + name = name.replaceAll(/[^a-zA-Z0-9_]/g, "_"); + // Append $ if the variable name would start with a numeric + if (name.match(/^[0-9]/)) name = "$" + name; + return toPascalCase(name); } function formatMiddlewarePath(path: string): string { diff --git a/src/server/types.ts b/src/server/types.ts index b353dcc4fdf..eca7ce847dd 100644 --- a/src/server/types.ts +++ b/src/server/types.ts @@ -106,6 +106,11 @@ export interface FreshConfig { * @deprecated Use `server.onListen` instead */ onListen?: (params: { hostname: string; port: number }) => void; + + /** + * URLs of islands that are imported + */ + islandUrls?: string[]; } export interface InternalFreshState { diff --git a/tests/fixture_island_url/deno.json b/tests/fixture_island_url/deno.json new file mode 100644 index 00000000000..fe6caa19a39 --- /dev/null +++ b/tests/fixture_island_url/deno.json @@ -0,0 +1,14 @@ +{ + "lock": false, + "imports": { + "$fresh/": "../../", + "preact": "https://esm.sh/preact@10.15.1", + "preact/": "https://esm.sh/preact@10.15.1/", + "@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_url/dev.ts b/tests/fixture_island_url/dev.ts new file mode 100755 index 00000000000..38a43e8be94 --- /dev/null +++ b/tests/fixture_island_url/dev.ts @@ -0,0 +1,10 @@ +#!/usr/bin/env -S deno run -A --watch=static/,routes/ + +import dev from "$fresh/dev.ts"; + +await dev(import.meta.url, "./main.ts", { + islandUrls: [ + "https://deno.land/x/fresh@1.5.2/tests/fixture/islands/Counter.tsx", + "https://deno.land/x/fresh@1.5.2/tests/fixture/islands/folder/Counter.tsx", + ], +}); diff --git a/tests/fixture_island_url/fresh.gen.ts b/tests/fixture_island_url/fresh.gen.ts new file mode 100644 index 00000000000..7518887ceb4 --- /dev/null +++ b/tests/fixture_island_url/fresh.gen.ts @@ -0,0 +1,21 @@ +// 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/index.tsx"; +import * as $$0 from "https://deno.land/x/fresh@1.5.2/tests/fixture/islands/Counter.tsx"; +import * as $$1 from "https://deno.land/x/fresh@1.5.2/tests/fixture/islands/folder/Counter.tsx"; + +const manifest = { + routes: { + "./routes/index.tsx": $0, + }, + islands: { + "https://deno.land/x/fresh@1.5.2/tests/fixture/islands/Counter.tsx": $$0, + "https://deno.land/x/fresh@1.5.2/tests/fixture/islands/folder/Counter.tsx": + $$1, + }, + baseUrl: import.meta.url, +}; + +export default manifest; diff --git a/tests/fixture_island_url/main.ts b/tests/fixture_island_url/main.ts new file mode 100644 index 00000000000..dedce9cbb04 --- /dev/null +++ b/tests/fixture_island_url/main.ts @@ -0,0 +1,10 @@ +/// +/// +/// +/// +/// + +import { start } from "$fresh/server.ts"; +import manifest from "./fresh.gen.ts"; + +await start(manifest); diff --git a/tests/fixture_island_url/routes/index.tsx b/tests/fixture_island_url/routes/index.tsx new file mode 100644 index 00000000000..a392db0e9b9 --- /dev/null +++ b/tests/fixture_island_url/routes/index.tsx @@ -0,0 +1,14 @@ +import { useSignal } from "@preact/signals"; +import Counter from "https://deno.land/x/fresh@1.5.2/tests/fixture/islands/Counter.tsx"; +import Counter2 from "https://deno.land/x/fresh@1.5.2/tests/fixture/islands/folder/Counter.tsx"; + +export default function Home() { + const count = useSignal(3); + const count2 = useSignal(10); + return ( +
+ + +
+ ); +} diff --git a/tests/islands_test.ts b/tests/islands_test.ts index fd9097e3822..cc586e95878 100644 --- a/tests/islands_test.ts +++ b/tests/islands_test.ts @@ -435,3 +435,26 @@ Deno.test("throws when passing non-jsx children to an island", async (t) => { }, ); }); + +Deno.test("island imported from url", async (t) => { + await withPageName( + "./tests/fixture_island_url/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); + + await t.step("Ensure 2 islands on 1 page are revived", async () => { + await counterTest("counter1", 3); + await counterTest("counter2", 10); + }); + }, + ); +}); From d728f5248dec250d493b77fd43769f011f530891 Mon Sep 17 00:00:00 2001 From: Mike White <64708423+mikehw@users.noreply.github.com> Date: Thu, 2 Nov 2023 12:05:22 -0500 Subject: [PATCH 2/4] Switch to comment hints instead of config --- docs/canary/concepts/islands.md | 22 +++++------ src/dev/dev_command.ts | 1 - src/dev/mod.ts | 39 +++++++++++++++---- src/server/types.ts | 5 --- .../deno.json | 0 tests/fixture_island_hints/dev.ts | 5 +++ .../fresh.gen.ts | 12 ++++-- .../main.ts | 0 .../routes/index.tsx | 6 +++ .../routes/sub-route/index.tsx | 20 ++++++++++ tests/fixture_island_url/dev.ts | 10 ----- tests/islands_test.ts | 13 ++++++- 12 files changed, 92 insertions(+), 41 deletions(-) rename tests/{fixture_island_url => fixture_island_hints}/deno.json (100%) create mode 100755 tests/fixture_island_hints/dev.ts rename tests/{fixture_island_url => fixture_island_hints}/fresh.gen.ts (69%) rename tests/{fixture_island_url => fixture_island_hints}/main.ts (100%) rename tests/{fixture_island_url => fixture_island_hints}/routes/index.tsx (70%) create mode 100644 tests/fixture_island_hints/routes/sub-route/index.tsx delete mode 100755 tests/fixture_island_url/dev.ts diff --git a/docs/canary/concepts/islands.md b/docs/canary/concepts/islands.md index 0e3f0550cf4..718e0334703 100644 --- a/docs/canary/concepts/islands.md +++ b/docs/canary/concepts/islands.md @@ -38,24 +38,24 @@ export default function Home() { } ``` -Islands may also be imported by URL. This is useful for using islands from -shared libraries. Island URLs must be listed in the FreshConfig. - -```ts dev.ts -await dev(import.meta.url, "./main.ts", { - islandUrls: [ - "https://deno.land/x/fresh@1.5.2/demo/islands/Counter.tsx", - ], -}); -``` +To use islands outside of the islands directory, use the `@fresh-island` hint +and the import will be treated as an island. This works for URL imports as well as relative imports. ```tsx route/index.tsx import { useSignal } from "@preact/signals"; +// @fresh-island import Counter from "https://deno.land/x/fresh@1.5.2/demo/islands/Counter.tsx"; +// @fresh-island +import SharedIsland from "../../shared/islands/SharedIsland.tsx"; export default function Home() { const count = useSignal(3); - return ; + return ( +
+ + +
+ ); } ``` diff --git a/src/dev/dev_command.ts b/src/dev/dev_command.ts index 0ee138dcbab..58caba55fb0 100644 --- a/src/dev/dev_command.ts +++ b/src/dev/dev_command.ts @@ -29,7 +29,6 @@ export async function dev( const newManifest = await collect( dir, config?.router?.ignoreFilePattern, - config?.islandUrls, ); Deno.env.set("FRSH_DEV_PREVIOUS_MANIFEST", JSON.stringify(newManifest)); diff --git a/src/dev/mod.ts b/src/dev/mod.ts index da56e97ddb9..14af28429da 100644 --- a/src/dev/mod.ts +++ b/src/dev/mod.ts @@ -1,4 +1,5 @@ import { + dirname, gte, isWindows, join, @@ -11,6 +12,8 @@ import { import { error } from "./error.ts"; const MIN_DENO_VERSION = "1.31.0"; const TEST_FILE_PATTERN = /[._]test\.(?:[tj]sx?|[mc][tj]s)$/; +const ISLAND_IMPORT_REGEX = + /import\s+(?:{[^}]+}\s+)?(?:[\w,]+\s+as\s+\w+\s+)?\w+\s+from\s+["']([^"']+)["']/; export function ensureMinDenoVersion() { // Check that the minimum supported Deno version is being used. @@ -64,12 +67,11 @@ const GROUP_REG = /[/\\\\]\((_[^/\\\\]+)\)[/\\\\]/; export async function collect( directory: string, ignoreFilePattern?: RegExp, - islandUrls?: string[], ): Promise { const filePaths = new Set(); const routes: string[] = []; - const islands: string[] = []; + const islandsSet: Set = new Set(); await Promise.all([ collectDir(join(directory, "./routes"), (entry, dir) => { const rel = join("routes", relative(dir, entry.path)); @@ -81,7 +83,7 @@ export async function collect( const match = normalized.match(GROUP_REG); if (match && match[1].startsWith("_")) { if (match[1] === "_islands") { - islands.push(rel); + islandsSet.add(rel); } return; } @@ -95,14 +97,36 @@ export async function collect( routes.push(rel); }, ignoreFilePattern), collectDir(join(directory, "./islands"), (entry) => { - islands.push(getManifestItemFromPath(directory, entry.path)); + islandsSet.add(getManifestItemFromPath(directory, entry.path)); }, ignoreFilePattern), + collectDir(directory, (entry, _) => { + const file = Deno.readTextFileSync(entry.path); + const lines = file.split("\n"); + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + if (line.match(/\/\/+\s*@fresh-island\s*/)) { + const islandImportLine = lines[i + 1]; + const matches = islandImportLine.match(ISLAND_IMPORT_REGEX); + if (matches) { + const match = matches[1]; + if (isURL(match)) { + islandsSet.add(match); + } else { + const fileDir = dirname(entry.path); + const resolvedPath = join(fileDir, matches[1]); + const relativePath = relative(directory, resolvedPath); + islandsSet.add(getManifestItemFromPath(directory, relativePath)); + } + } + } + } + }), ]); - if (islandUrls) { - islands.push(...islandUrls); - } routes.sort(); + // Remove duplicate islands + const islands = []; + islands.push(...islandsSet); islands.sort(); return { routes, islands }; @@ -211,6 +235,5 @@ function getManifestItemFromPath(directory: string, filePath: string) { if (isWindows) { relativePath = relativePath.replaceAll(SEP, posix.sep); } - return relativePath; } diff --git a/src/server/types.ts b/src/server/types.ts index eca7ce847dd..b353dcc4fdf 100644 --- a/src/server/types.ts +++ b/src/server/types.ts @@ -106,11 +106,6 @@ export interface FreshConfig { * @deprecated Use `server.onListen` instead */ onListen?: (params: { hostname: string; port: number }) => void; - - /** - * URLs of islands that are imported - */ - islandUrls?: string[]; } export interface InternalFreshState { diff --git a/tests/fixture_island_url/deno.json b/tests/fixture_island_hints/deno.json similarity index 100% rename from tests/fixture_island_url/deno.json rename to tests/fixture_island_hints/deno.json diff --git a/tests/fixture_island_hints/dev.ts b/tests/fixture_island_hints/dev.ts new file mode 100755 index 00000000000..2d85d6c183c --- /dev/null +++ b/tests/fixture_island_hints/dev.ts @@ -0,0 +1,5 @@ +#!/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_url/fresh.gen.ts b/tests/fixture_island_hints/fresh.gen.ts similarity index 69% rename from tests/fixture_island_url/fresh.gen.ts rename to tests/fixture_island_hints/fresh.gen.ts index 7518887ceb4..71978642ac5 100644 --- a/tests/fixture_island_url/fresh.gen.ts +++ b/tests/fixture_island_hints/fresh.gen.ts @@ -3,17 +3,21 @@ // This file is automatically updated during development when running `dev.ts`. import * as $0 from "./routes/index.tsx"; -import * as $$0 from "https://deno.land/x/fresh@1.5.2/tests/fixture/islands/Counter.tsx"; -import * as $$1 from "https://deno.land/x/fresh@1.5.2/tests/fixture/islands/folder/Counter.tsx"; +import * as $1 from "./routes/sub-route/index.tsx"; +import * as $$0 from "../fixture/islands/Counter.tsx"; +import * as $$1 from "https://deno.land/x/fresh@1.5.2/tests/fixture/islands/Counter.tsx"; +import * as $$2 from "https://deno.land/x/fresh@1.5.2/tests/fixture/islands/folder/Counter.tsx"; const manifest = { routes: { "./routes/index.tsx": $0, + "./routes/sub-route/index.tsx": $1, }, islands: { - "https://deno.land/x/fresh@1.5.2/tests/fixture/islands/Counter.tsx": $$0, + "../fixture/islands/Counter.tsx": $$0, + "https://deno.land/x/fresh@1.5.2/tests/fixture/islands/Counter.tsx": $$1, "https://deno.land/x/fresh@1.5.2/tests/fixture/islands/folder/Counter.tsx": - $$1, + $$2, }, baseUrl: import.meta.url, }; diff --git a/tests/fixture_island_url/main.ts b/tests/fixture_island_hints/main.ts similarity index 100% rename from tests/fixture_island_url/main.ts rename to tests/fixture_island_hints/main.ts diff --git a/tests/fixture_island_url/routes/index.tsx b/tests/fixture_island_hints/routes/index.tsx similarity index 70% rename from tests/fixture_island_url/routes/index.tsx rename to tests/fixture_island_hints/routes/index.tsx index a392db0e9b9..08469faf63f 100644 --- a/tests/fixture_island_url/routes/index.tsx +++ b/tests/fixture_island_hints/routes/index.tsx @@ -1,14 +1,20 @@ import { useSignal } from "@preact/signals"; +// @fresh-island import Counter from "https://deno.land/x/fresh@1.5.2/tests/fixture/islands/Counter.tsx"; +// @fresh-island import Counter2 from "https://deno.land/x/fresh@1.5.2/tests/fixture/islands/folder/Counter.tsx"; +// @fresh-island +import Counter3 from "../../fixture/islands/Counter.tsx"; export default function Home() { const count = useSignal(3); const count2 = useSignal(10); + const count3 = useSignal(10); return (
+
); } diff --git a/tests/fixture_island_hints/routes/sub-route/index.tsx b/tests/fixture_island_hints/routes/sub-route/index.tsx new file mode 100644 index 00000000000..3662c152d90 --- /dev/null +++ b/tests/fixture_island_hints/routes/sub-route/index.tsx @@ -0,0 +1,20 @@ +import { useSignal } from "@preact/signals"; +// @fresh-island +import Counter from "https://deno.land/x/fresh@1.5.2/tests/fixture/islands/Counter.tsx"; +// @fresh-island +import Counter2 from "https://deno.land/x/fresh@1.5.2/tests/fixture/islands/folder/Counter.tsx"; +// @fresh-island +import Counter3 from "../../../fixture/islands/Counter.tsx"; + +export default function Home() { + const count = useSignal(3); + const count2 = useSignal(10); + const count3 = useSignal(10); + return ( +
+ + + +
+ ); +} diff --git a/tests/fixture_island_url/dev.ts b/tests/fixture_island_url/dev.ts deleted file mode 100755 index 38a43e8be94..00000000000 --- a/tests/fixture_island_url/dev.ts +++ /dev/null @@ -1,10 +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", { - islandUrls: [ - "https://deno.land/x/fresh@1.5.2/tests/fixture/islands/Counter.tsx", - "https://deno.land/x/fresh@1.5.2/tests/fixture/islands/folder/Counter.tsx", - ], -}); diff --git a/tests/islands_test.ts b/tests/islands_test.ts index cc586e95878..e69ae60df59 100644 --- a/tests/islands_test.ts +++ b/tests/islands_test.ts @@ -436,7 +436,7 @@ Deno.test("throws when passing non-jsx children to an island", async (t) => { ); }); -Deno.test("island imported from url", async (t) => { +Deno.test("island imported from hint", async (t) => { await withPageName( "./tests/fixture_island_url/main.ts", async (page, address) => { @@ -451,9 +451,18 @@ Deno.test("island imported from url", async (t) => { } await page.goto(address); - await t.step("Ensure 2 islands on 1 page are revived", async () => { + await t.step("Ensure 3 islands on 1 page are revived", async () => { await counterTest("counter1", 3); await counterTest("counter2", 10); + await counterTest("counter3", 10); + }); + + await page.goto(`${address}/sub-route`); + + await t.step("Ensure 3 islands on a sub-route are revived", async () => { + await counterTest("counter1", 3); + await counterTest("counter2", 10); + await counterTest("counter3", 10); }); }, ); From a79952325eb5741f790abf6fbb5b27e10ec6acdf Mon Sep 17 00:00:00 2001 From: Mike White <64708423+mikehw@users.noreply.github.com> Date: Thu, 2 Nov 2023 12:10:32 -0500 Subject: [PATCH 3/4] lint --- docs/canary/concepts/islands.md | 3 ++- src/dev/dev_command.ts | 5 +---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/docs/canary/concepts/islands.md b/docs/canary/concepts/islands.md index 718e0334703..cbff7849c46 100644 --- a/docs/canary/concepts/islands.md +++ b/docs/canary/concepts/islands.md @@ -39,7 +39,8 @@ export default function Home() { ``` To use islands outside of the islands directory, use the `@fresh-island` hint -and the import will be treated as an island. This works for URL imports as well as relative imports. +and the import will be treated as an island. This works for URL imports as well +as relative imports. ```tsx route/index.tsx import { useSignal } from "@preact/signals"; diff --git a/src/dev/dev_command.ts b/src/dev/dev_command.ts index 58caba55fb0..7d840b09123 100644 --- a/src/dev/dev_command.ts +++ b/src/dev/dev_command.ts @@ -26,10 +26,7 @@ export async function dev( } else { currentManifest = { islands: [], routes: [] }; } - const newManifest = await collect( - dir, - config?.router?.ignoreFilePattern, - ); + const newManifest = await collect(dir, config?.router?.ignoreFilePattern); Deno.env.set("FRSH_DEV_PREVIOUS_MANIFEST", JSON.stringify(newManifest)); const manifestChanged = From c57158cbab0363539330b9b72a9567876636a198 Mon Sep 17 00:00:00 2001 From: Mike White <64708423+mikehw@users.noreply.github.com> Date: Thu, 2 Nov 2023 12:15:13 -0500 Subject: [PATCH 4/4] Fix test --- tests/fixture_island_hints/routes/index.tsx | 2 +- tests/fixture_island_hints/routes/sub-route/index.tsx | 2 +- tests/islands_test.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/fixture_island_hints/routes/index.tsx b/tests/fixture_island_hints/routes/index.tsx index 08469faf63f..2a853709496 100644 --- a/tests/fixture_island_hints/routes/index.tsx +++ b/tests/fixture_island_hints/routes/index.tsx @@ -14,7 +14,7 @@ export default function Home() {
- +
); } diff --git a/tests/fixture_island_hints/routes/sub-route/index.tsx b/tests/fixture_island_hints/routes/sub-route/index.tsx index 3662c152d90..da58cbc8107 100644 --- a/tests/fixture_island_hints/routes/sub-route/index.tsx +++ b/tests/fixture_island_hints/routes/sub-route/index.tsx @@ -14,7 +14,7 @@ export default function Home() {
- +
); } diff --git a/tests/islands_test.ts b/tests/islands_test.ts index e69ae60df59..38410bf04da 100644 --- a/tests/islands_test.ts +++ b/tests/islands_test.ts @@ -438,7 +438,7 @@ Deno.test("throws when passing non-jsx children to an island", async (t) => { Deno.test("island imported from hint", async (t) => { await withPageName( - "./tests/fixture_island_url/main.ts", + "./tests/fixture_island_hints/main.ts", async (page, address) => { async function counterTest(counterId: string, originalValue: number) { const pElem = await page.waitForSelector(`#${counterId} > p`);