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() {