Skip to content

Commit

Permalink
feat: improve islands handling
Browse files Browse the repository at this point in the history
  • Loading branch information
marvinhagemeister committed Mar 29, 2024
1 parent e2f705a commit 677d58b
Show file tree
Hide file tree
Showing 723 changed files with 1,028 additions and 590 deletions.
3 changes: 1 addition & 2 deletions deno.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
"test:www": "deno test -A tests/www/",
"manifests": "deno run -A genAllManifest.ts"
},
"exclude": ["**/_fresh/*", "**/tmp/*"],
"exclude": ["**/_fresh/*", "**/tmp/*", "*/tests_OLD/**"],
"publish": {
"include": [
"src/**",
Expand All @@ -44,7 +44,6 @@
"@fresh/plugin-tailwind": "./plugin-tailwindcss/src/mod.ts",
"@fresh/core/runtime": "./src/runtime/client/mod.tsx",
"@fresh/core/runtime-dev": "./src/runtime/client/dev.ts",
"@fresh/server": "./src/mod.ts",
"@luca/esbuild-deno-loader": "jsr:@luca/esbuild-deno-loader@^0.10.3",
"@preact/signals": "https://esm.sh/@preact/[email protected]?external=preact",
"@std/cli": "jsr:@std/cli@^0.221.0",
Expand Down
10 changes: 7 additions & 3 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,14 +81,17 @@ export class FreshApp<State> implements App<State> {
island(
filePathOrUrl: string | URL,
exportName: string,
fn: ComponentType,
): void {
// deno-lint-ignore no-explicit-any
fn: ComponentType<any>,
): this {
const filePath = filePathOrUrl instanceof URL
? filePathOrUrl.href
: filePathOrUrl;

// Create unique island name
let name = path.basename(filePath, path.extname(filePath));
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}`)) {
Expand All @@ -98,6 +101,7 @@ export class FreshApp<State> implements App<State> {
}

GLOBAL_ISLANDS.set(fn, { fn, exportName, name, file: filePathOrUrl });
return this;
}

use(middleware: Middleware<State>): this {
Expand Down
27 changes: 22 additions & 5 deletions src/dev/dev_app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
type App,
FreshApp,
GLOBAL_ISLANDS,
type Island,
type ListenOptions,
} from "../app.ts";
import type { FreshConfig } from "../config.ts";
Expand Down Expand Up @@ -119,12 +120,21 @@ export class FreshDevApp<T> extends FreshApp<T> implements DevApp<T> {
? "@fresh/core/runtime-dev"
: "@fresh/core/runtime",
};
const seenEntries = new Map<string, Island>();
const mapIslandToEntry = new Map<Island, string>();
for (const island of GLOBAL_ISLANDS.values()) {
const filePath = island.file instanceof URL
? island.file.href
: island.file;

entryPoints[island.name] = filePath;
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(this.config.root);
Expand Down Expand Up @@ -152,10 +162,16 @@ export class FreshDevApp<T> extends FreshApp<T> implements DevApp<T> {
await buildCache.addProcessedFile(pathname, file.contents, file.hash);
}

for (const [entry, chunkName] of output.entryToChunk.entries()) {
buildCache.islands.set(entry, `/${chunkName}`);
// 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();

const duration = Date.now() - start;
Expand All @@ -173,13 +189,14 @@ export class FreshDevApp<T> extends FreshApp<T> implements DevApp<T> {
export function getFreePort(
startPort: number,
hostname: string,
max: number = 20,
): number {
// 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 = startPort; port < startPort + 20; port++) {
for (let port = startPort; port < startPort + max; port++) {
try {
const listener = Deno.listen({ port, hostname });
listener.close();
Expand Down
2 changes: 2 additions & 0 deletions src/dev/dev_build_cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ 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";

export interface MemoryFile {
hash: string | null;
Expand Down Expand Up @@ -121,6 +122,7 @@ export class DiskBuildCache implements DevBuildCache {
throw new Error(`Path "${filePath}" resolved outside of "${outDir}"`);
}

await fsAdapter.mkdirp(path.dirname(filePath));
await Deno.writeFile(filePath, content);
}

Expand Down
4 changes: 4 additions & 0 deletions src/dev/esbuild.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,10 @@ export async function bundleJs(
}
}

if (!options.dev) {
await esbuild.stop();
}

return {
files,
entryToChunk,
Expand Down
11 changes: 11 additions & 0 deletions src/jsonify/custom_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,14 @@ Deno.test("custom stringify - Signals", () => {
'[["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]',
);
});
12 changes: 6 additions & 6 deletions src/jsonify/stringify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ function serializeInner(
out: string[],
indexes: Map<unknown, number>,
value: unknown,
custom?: Stringifiers,
custom: Stringifiers | undefined,
): number {
const seenIdx = indexes.get(value);
if (seenIdx !== undefined) return seenIdx;
Expand Down Expand Up @@ -75,7 +75,7 @@ function serializeInner(
str += "[";
for (let i = 0; i < value.length; i++) {
if (i in value) {
str += serializeInner(out, indexes, value[i]);
str += serializeInner(out, indexes, value[i], custom);
} else {
str += HOLE;
}
Expand Down Expand Up @@ -111,15 +111,15 @@ function serializeInner(
const items = new Array(value.size);
let i = 0;
value.forEach((v) => {
items[i++] = serializeInner(out, indexes, 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);
items[i++] = serializeInner(out, indexes, v);
items[i++] = serializeInner(out, indexes, k, custom);
items[i++] = serializeInner(out, indexes, v, custom);
});
str += `["Map",[${items.join(",")}]]`;
} else {
Expand All @@ -129,7 +129,7 @@ function serializeInner(
const key = keys[i];
str += JSON.stringify(key) + ":";
// deno-lint-ignore no-explicit-any
str += serializeInner(out, indexes, (value as any)[key]);
str += serializeInner(out, indexes, (value as any)[key], custom);

if (i < keys.length - 1) {
str += ",";
Expand Down
2 changes: 1 addition & 1 deletion src/middlewares/mod_test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { runMiddlewares } from "./mod.ts";
import { expect } from "@std/expect";
import { serveMiddleware } from "../test_utils.ts";
import type { Middleware } from "@fresh/server";
import type { Middleware } from "./mod.ts";

Deno.test("runMiddleware", async () => {
const middlewares: Middleware<{ text: string }>[] = [
Expand Down
2 changes: 1 addition & 1 deletion src/middlewares/static_files_test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { freshStaticFiles } from "@fresh/server";
import { freshStaticFiles } from "@fresh/core";
import { serveMiddleware } from "../test_utils.ts";
import type { BuildCache, StaticFile } from "../build_cache.ts";
import { expect } from "@std/expect";
Expand Down
14 changes: 8 additions & 6 deletions src/runtime/client/reviver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ const customParser: CustomParser = {

export function boot(
initialIslands: Record<string, ComponentType>,
islandProps: string[],
islandProps: string,
) {
const ctx: ReviveContext = {
islands: [],
Expand All @@ -69,14 +69,16 @@ export function boot(
ISLAND_REGISTRY.set(name, initialIslands[name]);
}

// deno-lint-ignore no-explicit-any
const allProps = parse<{ props: Record<string, unknown>; slots: any[] }[]>(
islandProps,
customParser,
);
for (let i = 0; i < ctx.islands.length; i++) {
const island = ctx.islands[i];

const props = parse<Record<string, unknown>>(
islandProps[island.propsIdx],
customParser,
);
revive(island, props);
const islandConfig = allProps[island.propsIdx];
revive(island, islandConfig.props);
}
}

Expand Down
15 changes: 10 additions & 5 deletions src/runtime/server/mod.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,11 @@ export const FreshScripts: () => VNode = ((

const islandImports = islandArr.map((island) => {
const chunk = ctx.buildCache.getIslandChunkName(island.name);
if (chunk === null) {
throw new Error(
`Could not find chunk for ${island.name} ${island.exportName}#${island.file}`,
);
}
const named = island.exportName === "default"
? island.name
: `{ ${island.exportName} }`;
Expand All @@ -80,18 +85,18 @@ export const FreshScripts: () => VNode = ((
.join(",") +
"}";

const serializedProps = islandProps.map((props) => {
return `'${stringify(props.props, stringifiers)}'`;
}).join(",");
const serializedProps = stringify(islandProps, stringifiers);

const scriptContent =
`import { boot } from "${basePath}/fresh-runtime.js";${islandImports}boot(${islandObj},\`${serializedProps}\`);`;

// FIXME: integrity
// FIXME: nonce
return (
<script
type="module"
dangerouslySetInnerHTML={{
__html:
`import { boot } from "${basePath}/fresh-runtime.js";${islandImports}boot(${islandObj},[${serializedProps}]);`,
__html: scriptContent,
}}
>
</script>
Expand Down
26 changes: 26 additions & 0 deletions tests/fixtures_islands/Counter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Signal, useSignal } from "@preact/signals";
import { useEffect } from "preact/hooks";

export interface CounterProps {
id?: string;
count: Signal<number>;
}

export function Counter(props: CounterProps) {
const active = useSignal(false);
useEffect(() => {
active.value = true;
}, []);

return (
<div id={props.id} class={active.value ? "ready" : ""}>
<button class="decrement" onClick={() => props.count.value -= 1}>
-1
</button>
<p class="output">{props.count}</p>
<button class="increment" onClick={() => props.count.value += 1}>
+1
</button>
</div>
);
}
5 changes: 5 additions & 0 deletions tests/fixtures_islands/JsonIsland.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import data from "./data.json" with { type: "json" };

export function JsonIsland() {
return <pre>{JSON.stringify(data)}</pre>;
}
17 changes: 17 additions & 0 deletions tests/fixtures_islands/Multiple.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { ComponentChildren } from "preact";
import { Counter } from "./Counter.tsx";
import { useSignal } from "@preact/signals";

export interface MultipleProps {
id?: string;
children?: ComponentChildren;
}

export function Multiple1(props: MultipleProps) {
const sig = useSignal(0);
return <Counter id={props.id} count={sig} />;
}
export function Multiple2(props: MultipleProps) {
const sig = useSignal(0);
return <Counter id={props.id} count={sig} />;
}
10 changes: 10 additions & 0 deletions tests/fixtures_islands/NullIsland.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { useEffect } from "preact/hooks";

export function NullIsland() {
useEffect(() => {
const div = document.createElement("div");
div.className = "ready";
document.body.appendChild(div);
}, []);
return null;
}
3 changes: 3 additions & 0 deletions tests/fixtures_islands/data.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"foo": 123
}
Loading

0 comments on commit 677d58b

Please sign in to comment.