Skip to content

Commit

Permalink
Merge pull request #20 from bholmesdev/feat/simple-stack-stream
Browse files Browse the repository at this point in the history
Welcome simple-stack-stream
  • Loading branch information
bholmesdev authored Jan 2, 2024
2 parents 68747cc + 0ab8cd1 commit ddcb25f
Show file tree
Hide file tree
Showing 33 changed files with 318 additions and 384 deletions.
5 changes: 5 additions & 0 deletions .changeset/pink-rice-behave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"simple-stack-stream": patch
---

Simple stream initial release. Who said suspense had to be hard?
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { defineConfig } from "astro/config";
import simpleStackForm from "simple-stack-form";
import simpleStackStream from "simple-stack-stream";
import react from "@astrojs/react";
import node from "@astrojs/node";
import preact from "@astrojs/preact";
Expand All @@ -10,6 +11,7 @@ export default defineConfig({
output: "server",
integrations: [
simpleStackForm(),
simpleStackStream(),
react({ include: ["**/react/*"] }),
preact({ include: ["**/preact/*"] }),
tailwind(),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "@examples/form",
"name": "@examples/playground",
"type": "module",
"version": "0.0.0",
"private": true,
Expand All @@ -24,6 +24,7 @@
"react-dom": "^18.0.0",
"sanitize-html": "^2.11.0",
"simple-stack-form": "^0.1.0",
"simple-stack-stream": "^0.0.1",
"tailwindcss": "^3.0.24",
"zod": "^3.22.4"
},
Expand Down
File renamed without changes
File renamed without changes.
14 changes: 14 additions & 0 deletions examples/playground/src/components/Wait.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
type Props = {
ms: number,
class?: string,
}
const {ms, class: cls} = Astro.props;
await new Promise(resolve => setTimeout(resolve, ms));
---
<div class:list={["rounded p-2 border-gray-300 border-2", cls]}>
<slot />
<p class="italic text-gray-600 text-sm">Loaded in {ms}ms</p>
</div>
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
/// <reference path="../.astro/types.d.ts" />
/// <reference types="astro/client" />
/// <reference types="simple-stack-form/types" />
File renamed without changes.
File renamed without changes.
35 changes: 35 additions & 0 deletions examples/playground/src/pages/stream.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
---
import Wait from "../components/Wait.astro";
import { Suspense, ResolveSuspended } from 'simple-stack-stream/components'
---

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Suspense</title>
</head>
<body>
<h1>Out of order streaming</h1>
<!-- out-of-order streaming: fallback,
JS to swap content -->
<Suspense>
<Wait ms={2000}>
<p class="font-bold text-xl">Content</p>
</Wait>
<p slot="fallback">Loading...</p>
</Suspense>

<!-- in-order HTML streaming (no client JS) -->
<Wait class="fixed bottom-0 left-0 right-0" ms={500}>
<footer>
<p>Follow us</p>
<p>Join the newsletter</p>
</footer>
</Wait>

<!-- render all suspended content -->
<ResolveSuspended />
</body>
</html>
File renamed without changes.
File renamed without changes.
6 changes: 3 additions & 3 deletions packages/form/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -244,8 +244,8 @@ We expose internal functions to manage your form state and handle both synchrono

An demo can be found in our repository `examples`:

- [StackBlitz playground](https://stackblitz.com/github/bholmesdev/simple-stack/tree/main/examples/form)
- [GitHub](https://github.com/bholmesdev/simple-stack/tree/main/examples/form)
- [StackBlitz playground](https://stackblitz.com/github/bholmesdev/simple-stack/tree/main/examples/playground)
- [GitHub](https://github.com/bholmesdev/simple-stack/tree/main/examples/playground)

## Sanitizing User Input

Expand All @@ -272,4 +272,4 @@ const signupForm = createForm({

### Examples

You can find a sanitization implementation example on our [`examples`](https://github.com/bholmesdev/simple-stack/tree/main/examples/form/src/components/Sanitize.tsx)
You can find a sanitization implementation example on our [`examples`](https://github.com/bholmesdev/simple-stack/tree/main/examples/playground/src/components/Sanitize.tsx)
89 changes: 89 additions & 0 deletions packages/stream/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# Simple stream 🌊

Suspend Astro components with fallback content. Like React Server Components, but Just HTML ™️

```astro
---
import { Suspense, ResolveSuspended } from 'simple-stack-stream/components';
---
<h1>Simple stream</h1>
https://github.com/bholmesdev/simple-stack/assets/51384119/99ed15a4-5a70-4f19-bc2a-712d4039c0a7
<!--Suspend slow-to-load content-->
<Suspense>
<VideoPlayer />
<!--Show fallback content-->
<LoadingSkeleton slot="fallback" />
</Suspense>
<Footer />
<!--Render suspended content-->
<ResolveSuspended />
```

## Installation

Simple stream is an Astro integration. You can install and configure this via the Astro CLI using `astro add`:

```bash
npm run astro add simple-stack-stream
```

## Usage

Simple stream exposes a "Suspense" utility to show fallback content while your server-side components load.

### `Suspense`

`<Suspense>` is a wrapper component for any content you want to load out-of-order with a fallback. Pass any suspended content as children, and use `slot="fallback"` to define your fallback:

```astro
---
import { Suspense } from 'simple-stack-stream/components';
---
<Suspense>
<VideoPlayer />
<p slot="fallback">Loading...</p>
</Suspense>
```

⚠️ **Client JS is required** for suspended content to render. For progressive enhancement, we recommend including `<noscript>` content as part of your fallback:

```astro
---
import { Suspense } from 'simple-stack-stream/components';
---
<Suspense>
<VideoPlayer />
<div slot="fallback">
<noscript>JavaScript is required for video playback.</noscript>
<p>Loading...</p>
</div>
</Suspense>
```

### `ResolveSuspended`

The `<ResolveSuspended />` component renders all suspended content. This component should be placed at the _end_ of your HTML document, ideally before the closing `</body>` tag. This prevents `ResolveSuspended` from blocking components below it when [using Astro SSR](https://docs.astro.build/en/guides/server-side-rendering/#html-streaming).

We recommend [a reusable Layout](https://docs.astro.build/en/core-concepts/layouts/) to ensure this component is present wherever `<Suspense>` is used:

```astro
---
// src/layouts/Layout.astro
import { ResolveSuspended } from 'simple-stack-form/components';
---
<!DOCTYPE html>
<html lang="en">
<head>...</head>
<body>
<slot />
<ResolveSuspended />
</body>
</html>
```
26 changes: 26 additions & 0 deletions packages/stream/components/ResolveSuspended.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
---
import type { LocalsWithStreamInternals } from "./types";
const { stream } = Astro.locals as LocalsWithStreamInternals;
const entries = [...stream._internal.components.entries()];
const resolvedEntries = await Promise.all(
entries.map(
async ([id, slotPromise]) => [id, await slotPromise],
)
);
---

{
resolvedEntries.map( ([id, html]) => (
<>
<template data-suspense-id={id} set:html={html} />
<script is:inline define:vars={{ id }}>
const template = document.querySelector(`[data-suspense-id="${id}"]`).content;
const dest = document.getElementById(id);
dest.replaceWith(template);
</script>
</>
))
}
16 changes: 16 additions & 0 deletions packages/stream/components/Suspense.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
import { customAlphabet, urlAlphabet } from "nanoid";
import type { LocalsWithStreamInternals } from "./types";
const safeId = customAlphabet(urlAlphabet, 10);
const slotPromise = Astro.slots.render('default');
const id = safeId();
const { stream } = Astro.locals as LocalsWithStreamInternals;
stream._internal.components.set(id, slotPromise);
---

<simple-suspense id={id}>
<slot name="fallback" />
</simple-suspense>
1 change: 1 addition & 0 deletions packages/stream/components/env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/// <reference types="astro/client" />
2 changes: 2 additions & 0 deletions packages/stream/components/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { default as ResolveSuspended } from "./ResolveSuspended.astro";
export { default as Suspense } from "./Suspense.astro";
3 changes: 3 additions & 0 deletions packages/stream/components/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extends": "astro/tsconfigs/strictest"
}
7 changes: 7 additions & 0 deletions packages/stream/components/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export type LocalsWithStreamInternals = {
stream: {
_internal: {
components: Map<string, Promise<string>>;
};
};
};
33 changes: 33 additions & 0 deletions packages/stream/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"name": "simple-stack-stream",
"version": "0.0.1",
"description": "Suspend your Astro components with fallback content.",
"type": "module",
"scripts": {
"build": "tsc",
"dev": "tsc --watch"
},
"exports": {
"./components": "./components/index.ts",
"./middleware": "./dist/middleware.js",
".": "./dist/index.js"
},
"keywords": ["withastro", "astro-integration"],
"repository": {
"type": "git",
"url": "https://github.com/bholmesdev/simple-stack.git",
"directory": "packages/form"
},
"devDependencies": {
"astro": "^4.0.7",
"typescript": "^5.3.3"
},
"peerDependencies": {
"astro": "^3.6.0 || ^4.0.0"
},
"author": "bholmesdev",
"license": "MIT",
"dependencies": {
"nanoid": "^5.0.3"
}
}
15 changes: 15 additions & 0 deletions packages/stream/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import type { AstroIntegration } from "astro";

export default function integration(): AstroIntegration {
return {
name: "simple-form",
hooks: {
"astro:config:setup"({ addMiddleware }) {
addMiddleware({
entrypoint: "simple-stack-stream/middleware",
order: "pre",
});
},
},
};
}
11 changes: 11 additions & 0 deletions packages/stream/src/middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { defineMiddleware } from "astro/middleware";

export const onRequest = defineMiddleware(({ request, locals }, next) => {
locals.stream = {
_internal: {
components: new Map(),
},
};

return next();
});
8 changes: 8 additions & 0 deletions packages/stream/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"extends": "../../tsconfig.base.json",
"include": ["src"],
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src"
}
}
Loading

0 comments on commit ddcb25f

Please sign in to comment.