i love deno-fmt
This commit is contained in:
parent
5197e3316d
commit
995161f491
9 changed files with 435 additions and 430 deletions
52
deno.json
52
deno.json
|
|
@ -1,26 +1,30 @@
|
|||
{
|
||||
"tasks": {
|
||||
"dev": "deno run --allow-net src/main.ts"
|
||||
},
|
||||
"imports": {
|
||||
"interest/jsx-runtime": "./src/jsx.ts"
|
||||
},
|
||||
"lint": {
|
||||
"rules": {
|
||||
"tags": [
|
||||
"recommended"
|
||||
],
|
||||
"exclude": ["no-explicit-any", "require-await"]
|
||||
}
|
||||
},
|
||||
"compilerOptions": {
|
||||
"jsx": "react-jsx",
|
||||
"jsxImportSource": "interest",
|
||||
"lib": [
|
||||
"deno.ns",
|
||||
"esnext",
|
||||
"dom",
|
||||
"dom.iterable"
|
||||
]
|
||||
}
|
||||
"tasks": {
|
||||
"dev": "deno run --allow-net src/main.ts"
|
||||
},
|
||||
"imports": {
|
||||
"interest/jsx-runtime": "./src/jsx.ts"
|
||||
},
|
||||
"fmt": {
|
||||
"useTabs": true,
|
||||
"semiColons": true
|
||||
},
|
||||
"lint": {
|
||||
"rules": {
|
||||
"tags": [
|
||||
"recommended"
|
||||
],
|
||||
"exclude": ["no-explicit-any", "require-await"]
|
||||
}
|
||||
},
|
||||
"compilerOptions": {
|
||||
"jsx": "react-jsx",
|
||||
"jsxImportSource": "interest",
|
||||
"lib": [
|
||||
"deno.ns",
|
||||
"esnext",
|
||||
"dom",
|
||||
"dom.iterable"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,17 +15,17 @@ const copyrightHeader = `/**
|
|||
const dir = "./";
|
||||
|
||||
for await (
|
||||
const entry of walk(dir, {
|
||||
exts: [".ts", ".tsx"],
|
||||
includeDirs: false,
|
||||
skip: [/node_modules/, /copyright\.ts$/],
|
||||
})
|
||||
const entry of walk(dir, {
|
||||
exts: [".ts", ".tsx"],
|
||||
includeDirs: false,
|
||||
skip: [/node_modules/, /copyright\.ts$/],
|
||||
})
|
||||
) {
|
||||
const filePath = entry.path;
|
||||
const content = await Deno.readTextFile(filePath);
|
||||
const filePath = entry.path;
|
||||
const content = await Deno.readTextFile(filePath);
|
||||
|
||||
if (!content.startsWith(copyrightHeader)) {
|
||||
await Deno.writeTextFile(filePath, copyrightHeader + "\n" + content);
|
||||
console.log(`Added header to ${filePath}`);
|
||||
}
|
||||
if (!content.startsWith(copyrightHeader)) {
|
||||
await Deno.writeTextFile(filePath, copyrightHeader + "\n" + content);
|
||||
console.log(`Added header to ${filePath}`);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
29
src/app.tsx
29
src/app.tsx
|
|
@ -6,21 +6,22 @@
|
|||
import { close, open } from "interest/jsx-runtime";
|
||||
|
||||
async function* Fruits() {
|
||||
const fruits = ["TSX", "Apple", "Banana", "Cherry"];
|
||||
yield open("ol");
|
||||
for (const fruit of fruits) {
|
||||
await new Promise((r) => setTimeout(r, 500));
|
||||
yield <li>{fruit}</li>;
|
||||
}
|
||||
yield close("ol");
|
||||
const fruits = ["TSX", "Apple", "Banana", "Cherry"];
|
||||
|
||||
yield open("ol");
|
||||
for (const fruit of fruits) {
|
||||
await new Promise((r) => setTimeout(r, 500));
|
||||
yield <li>{fruit}</li>;
|
||||
}
|
||||
yield close("ol");
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<>
|
||||
<h1>JSX Page</h1>
|
||||
<p class="oh hey">meowing chunk by chunk</p>
|
||||
<Fruits />
|
||||
</>
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<h1>JSX Page</h1>
|
||||
<p class="oh hey">meowing chunk by chunk</p>
|
||||
<Fruits />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
50
src/global.d.ts
vendored
50
src/global.d.ts
vendored
|
|
@ -8,34 +8,34 @@
|
|||
import type { JsxElement } from "interest/jsx-runtime";
|
||||
|
||||
type HTMLAttributeMap<T = HTMLElement> = Partial<
|
||||
Omit<T, keyof Element | "children" | "style"> & {
|
||||
style?: string;
|
||||
class?: string;
|
||||
children?: any;
|
||||
[key: `data-${string}`]: string | number | boolean | null | undefined;
|
||||
[key: `aria-${string}`]: string | number | boolean | null | undefined;
|
||||
}
|
||||
Omit<T, keyof Element | "children" | "style"> & {
|
||||
style?: string;
|
||||
class?: string;
|
||||
children?: any;
|
||||
[key: `data-${string}`]: string | number | boolean | null | undefined;
|
||||
[key: `aria-${string}`]: string | number | boolean | null | undefined;
|
||||
}
|
||||
>;
|
||||
|
||||
declare global {
|
||||
namespace JSX {
|
||||
type Element = JsxElement;
|
||||
namespace JSX {
|
||||
type Element = JsxElement;
|
||||
|
||||
export interface ElementChildrenAttribute {
|
||||
// deno-lint-ignore ban-types
|
||||
children: {};
|
||||
}
|
||||
export interface ElementChildrenAttribute {
|
||||
// deno-lint-ignore ban-types
|
||||
children: {};
|
||||
}
|
||||
|
||||
export type IntrinsicElements =
|
||||
& {
|
||||
[K in keyof HTMLElementTagNameMap]: HTMLAttributeMap<
|
||||
HTMLElementTagNameMap[K]
|
||||
>;
|
||||
}
|
||||
& {
|
||||
[K in keyof SVGElementTagNameMap]: HTMLAttributeMap<
|
||||
SVGElementTagNameMap[K]
|
||||
>;
|
||||
};
|
||||
}
|
||||
export type IntrinsicElements =
|
||||
& {
|
||||
[K in keyof HTMLElementTagNameMap]: HTMLAttributeMap<
|
||||
HTMLElementTagNameMap[K]
|
||||
>;
|
||||
}
|
||||
& {
|
||||
[K in keyof SVGElementTagNameMap]: HTMLAttributeMap<
|
||||
SVGElementTagNameMap[K]
|
||||
>;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
162
src/html.ts
162
src/html.ts
|
|
@ -7,131 +7,131 @@ import { type Chunk } from "./http.ts";
|
|||
import { ChunkedStream } from "./stream.ts";
|
||||
|
||||
export type Attrs = Record<
|
||||
string,
|
||||
string | number | boolean | null | undefined
|
||||
string,
|
||||
string | number | boolean | null | undefined
|
||||
>;
|
||||
|
||||
export const VOID_TAGS = new Set([
|
||||
"area",
|
||||
"base",
|
||||
"br",
|
||||
"col",
|
||||
"embed",
|
||||
"hr",
|
||||
"img",
|
||||
"input",
|
||||
"link",
|
||||
"meta",
|
||||
"param",
|
||||
"source",
|
||||
"track",
|
||||
"wbr",
|
||||
"area",
|
||||
"base",
|
||||
"br",
|
||||
"col",
|
||||
"embed",
|
||||
"hr",
|
||||
"img",
|
||||
"input",
|
||||
"link",
|
||||
"meta",
|
||||
"param",
|
||||
"source",
|
||||
"track",
|
||||
"wbr",
|
||||
]);
|
||||
|
||||
const ESCAPE_MAP: Record<string, string> = {
|
||||
"&": "&",
|
||||
"<": "<",
|
||||
">": ">",
|
||||
'"': """,
|
||||
"'": "'",
|
||||
"&": "&",
|
||||
"<": "<",
|
||||
">": ">",
|
||||
'"': """,
|
||||
"'": "'",
|
||||
};
|
||||
|
||||
export function escape(input: string): string {
|
||||
let result = "";
|
||||
let lastIndex = 0;
|
||||
let result = "";
|
||||
let lastIndex = 0;
|
||||
|
||||
for (let i = 0; i < input.length; i++) {
|
||||
const replacement = ESCAPE_MAP[input[i]];
|
||||
if (replacement) {
|
||||
result += input.slice(lastIndex, i) + replacement;
|
||||
lastIndex = i + 1;
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < input.length; i++) {
|
||||
const replacement = ESCAPE_MAP[input[i]];
|
||||
if (replacement) {
|
||||
result += input.slice(lastIndex, i) + replacement;
|
||||
lastIndex = i + 1;
|
||||
}
|
||||
}
|
||||
|
||||
return lastIndex ? result + input.slice(lastIndex) : input;
|
||||
return lastIndex ? result + input.slice(lastIndex) : input;
|
||||
}
|
||||
|
||||
function serialize(attrs: Attrs | undefined): string {
|
||||
if (!attrs) return "";
|
||||
let output = "";
|
||||
if (!attrs) return "";
|
||||
let output = "";
|
||||
|
||||
for (const key in attrs) {
|
||||
const val = attrs[key];
|
||||
if (val == null || val === false) continue;
|
||||
output += " ";
|
||||
output += val === true ? key : `${key}="${escape(String(val))}"`;
|
||||
}
|
||||
for (const key in attrs) {
|
||||
const val = attrs[key];
|
||||
if (val == null || val === false) continue;
|
||||
output += " ";
|
||||
output += val === true ? key : `${key}="${escape(String(val))}"`;
|
||||
}
|
||||
|
||||
return output;
|
||||
return output;
|
||||
}
|
||||
|
||||
type TagRes = void | Promise<void>;
|
||||
|
||||
type TagFn = {
|
||||
(attrs: Attrs, ...children: Chunk[]): TagRes;
|
||||
(attrs: Attrs, fn: () => any): TagRes;
|
||||
(...children: Chunk[]): TagRes;
|
||||
(template: TemplateStringsArray, ...values: Chunk[]): TagRes;
|
||||
(fn: () => any): TagRes;
|
||||
(attrs: Attrs, ...children: Chunk[]): TagRes;
|
||||
(attrs: Attrs, fn: () => any): TagRes;
|
||||
(...children: Chunk[]): TagRes;
|
||||
(template: TemplateStringsArray, ...values: Chunk[]): TagRes;
|
||||
(fn: () => any): TagRes;
|
||||
};
|
||||
|
||||
export type HtmlProxy = { [K in keyof HTMLElementTagNameMap]: TagFn } & {
|
||||
[key: string]: TagFn;
|
||||
[key: string]: TagFn;
|
||||
};
|
||||
|
||||
const isTemplateLiteral = (arg: any): arg is TemplateStringsArray =>
|
||||
Array.isArray(arg) && "raw" in arg;
|
||||
Array.isArray(arg) && "raw" in arg;
|
||||
|
||||
const isAttributes = (arg: any): arg is Record<string, any> =>
|
||||
arg && typeof arg === "object" && !isTemplateLiteral(arg) &&
|
||||
!Array.isArray(arg) && !(arg instanceof Promise);
|
||||
arg && typeof arg === "object" && !isTemplateLiteral(arg) &&
|
||||
!Array.isArray(arg) && !(arg instanceof Promise);
|
||||
|
||||
async function render(child: unknown): Promise<string> {
|
||||
if (child == null) return "";
|
||||
if (child == null) return "";
|
||||
|
||||
if (typeof child === "string") return escape(child);
|
||||
if (typeof child === "number") return String(child);
|
||||
if (typeof child === "boolean") return String(Number(child));
|
||||
if (typeof child === "string") return escape(child);
|
||||
if (typeof child === "number") return String(child);
|
||||
if (typeof child === "boolean") return String(Number(child));
|
||||
|
||||
if (child instanceof Promise) return render(await child);
|
||||
if (child instanceof Promise) return render(await child);
|
||||
|
||||
if (Array.isArray(child)) {
|
||||
return (await Promise.all(child.map(render))).join("");
|
||||
}
|
||||
if (Array.isArray(child)) {
|
||||
return (await Promise.all(child.map(render))).join("");
|
||||
}
|
||||
|
||||
if (typeof child === "function") return render(await child());
|
||||
return escape(String(child));
|
||||
if (typeof child === "function") return render(await child());
|
||||
return escape(String(child));
|
||||
}
|
||||
|
||||
export function html(chunks: ChunkedStream<string>): HtmlProxy {
|
||||
const cache = new Map<string, TagFn>();
|
||||
const write = (buf: string) => !chunks.closed && chunks.write(buf);
|
||||
const cache = new Map<string, TagFn>();
|
||||
const write = (buf: string) => !chunks.closed && chunks.write(buf);
|
||||
|
||||
const handler: ProxyHandler<Record<string, TagFn>> = {
|
||||
get(_, tag: string) {
|
||||
let fn = cache.get(tag);
|
||||
if (fn) return fn;
|
||||
const handler: ProxyHandler<Record<string, TagFn>> = {
|
||||
get(_, tag: string) {
|
||||
let fn = cache.get(tag);
|
||||
if (fn) return fn;
|
||||
|
||||
fn = async (...args: unknown[]) => {
|
||||
const attrs = isAttributes(args[0]) ? args.shift() : undefined;
|
||||
fn = async (...args: unknown[]) => {
|
||||
const attrs = isAttributes(args[0]) ? args.shift() : undefined;
|
||||
|
||||
const isVoid = VOID_TAGS.has(tag.toLowerCase());
|
||||
const attributes = serialize(attrs as Attrs);
|
||||
const isVoid = VOID_TAGS.has(tag.toLowerCase());
|
||||
const attributes = serialize(attrs as Attrs);
|
||||
|
||||
write(`<${tag}${attributes}${isVoid ? "/" : ""}>`);
|
||||
if (isVoid) return;
|
||||
write(`<${tag}${attributes}${isVoid ? "/" : ""}>`);
|
||||
if (isVoid) return;
|
||||
|
||||
for (const child of args) {
|
||||
write(await render(child));
|
||||
}
|
||||
for (const child of args) {
|
||||
write(await render(child));
|
||||
}
|
||||
|
||||
write(`</${tag}>`);
|
||||
};
|
||||
write(`</${tag}>`);
|
||||
};
|
||||
|
||||
return cache.set(tag, fn), fn;
|
||||
},
|
||||
};
|
||||
return cache.set(tag, fn), fn;
|
||||
},
|
||||
};
|
||||
|
||||
const proxy = new Proxy({}, handler) as HtmlProxy;
|
||||
return proxy;
|
||||
const proxy = new Proxy({}, handler) as HtmlProxy;
|
||||
return proxy;
|
||||
}
|
||||
|
|
|
|||
178
src/http.ts
178
src/http.ts
|
|
@ -6,127 +6,127 @@
|
|||
import { ChunkedStream } from "./stream.ts";
|
||||
|
||||
export interface StreamOptions {
|
||||
headContent?: string;
|
||||
bodyAttributes?: string;
|
||||
lang?: string;
|
||||
headContent?: string;
|
||||
bodyAttributes?: string;
|
||||
lang?: string;
|
||||
}
|
||||
|
||||
export type Chunk =
|
||||
| string
|
||||
| AsyncIterable<string>
|
||||
| Promise<string>
|
||||
| Iterable<string>
|
||||
| null
|
||||
| undefined;
|
||||
| string
|
||||
| AsyncIterable<string>
|
||||
| Promise<string>
|
||||
| Iterable<string>
|
||||
| null
|
||||
| undefined;
|
||||
|
||||
async function* normalize(
|
||||
value: Chunk | undefined | null,
|
||||
value: Chunk | undefined | null,
|
||||
): AsyncIterable<string> {
|
||||
if (value == null) return;
|
||||
if (value == null) return;
|
||||
|
||||
if (typeof value === "string") {
|
||||
yield value;
|
||||
} else if (value instanceof Promise) {
|
||||
const resolved = await value;
|
||||
if (resolved != null) yield String(resolved);
|
||||
} else if (Symbol.asyncIterator in value || Symbol.iterator in value) {
|
||||
for await (const chunk of value as AsyncIterable<string>) {
|
||||
if (chunk != null) yield String(chunk);
|
||||
}
|
||||
} else {
|
||||
yield String(value);
|
||||
}
|
||||
if (typeof value === "string") {
|
||||
yield value;
|
||||
} else if (value instanceof Promise) {
|
||||
const resolved = await value;
|
||||
if (resolved != null) yield String(resolved);
|
||||
} else if (Symbol.asyncIterator in value || Symbol.iterator in value) {
|
||||
for await (const chunk of value as AsyncIterable<string>) {
|
||||
if (chunk != null) yield String(chunk);
|
||||
}
|
||||
} else {
|
||||
yield String(value);
|
||||
}
|
||||
}
|
||||
|
||||
export type ChunkedWriter = (
|
||||
strings: TemplateStringsArray,
|
||||
...values: Chunk[]
|
||||
strings: TemplateStringsArray,
|
||||
...values: Chunk[]
|
||||
) => Promise<void>;
|
||||
|
||||
export const makeChunkWriter =
|
||||
(stream: ChunkedStream<string>): ChunkedWriter =>
|
||||
async (strings, ...values) => {
|
||||
const emit = (chunk: string) =>
|
||||
!stream.closed &&
|
||||
(chunk === "EOF" ? stream.close() : stream.write(chunk));
|
||||
(stream: ChunkedStream<string>): ChunkedWriter =>
|
||||
async (strings, ...values) => {
|
||||
const emit = (chunk: string) =>
|
||||
!stream.closed &&
|
||||
(chunk === "EOF" ? stream.close() : stream.write(chunk));
|
||||
|
||||
for (let i = 0; i < strings.length; i++) {
|
||||
strings[i] && emit(strings[i]);
|
||||
for (let i = 0; i < strings.length; i++) {
|
||||
strings[i] && emit(strings[i]);
|
||||
|
||||
for await (const chunk of normalize(values[i])) {
|
||||
emit(chunk);
|
||||
}
|
||||
}
|
||||
};
|
||||
for await (const chunk of normalize(values[i])) {
|
||||
emit(chunk);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export function chunkedHtml() {
|
||||
const chunks = new ChunkedStream<string>();
|
||||
const chunks = new ChunkedStream<string>();
|
||||
|
||||
const stream = new ReadableStream<Uint8Array>({
|
||||
async start(controller) {
|
||||
const encoder = new TextEncoder();
|
||||
try {
|
||||
for await (const chunk of chunks) {
|
||||
controller.enqueue(encoder.encode(chunk));
|
||||
}
|
||||
controller.close();
|
||||
} catch (error) {
|
||||
controller.error(error);
|
||||
}
|
||||
},
|
||||
cancel: chunks.close,
|
||||
});
|
||||
const stream = new ReadableStream<Uint8Array>({
|
||||
async start(controller) {
|
||||
const encoder = new TextEncoder();
|
||||
try {
|
||||
for await (const chunk of chunks) {
|
||||
controller.enqueue(encoder.encode(chunk));
|
||||
}
|
||||
controller.close();
|
||||
} catch (error) {
|
||||
controller.error(error);
|
||||
}
|
||||
},
|
||||
cancel: chunks.close,
|
||||
});
|
||||
|
||||
return { chunks, stream };
|
||||
return { chunks, stream };
|
||||
}
|
||||
|
||||
const DOCUMENT_TYPE = "<!DOCTYPE html>";
|
||||
const HTML_BEGIN = (lang: string) =>
|
||||
`<html lang="${lang}"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">`;
|
||||
`<html lang="${lang}"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">`;
|
||||
const HEAD_END = "</head><body";
|
||||
const BODY_END = ">";
|
||||
const HTML_END = "</body></html>";
|
||||
|
||||
export interface HtmlStream {
|
||||
write: ChunkedWriter;
|
||||
blob: ReadableStream<Uint8Array>;
|
||||
chunks: ChunkedStream<string>;
|
||||
close(): void;
|
||||
error(err: Error): void;
|
||||
readonly response: Response;
|
||||
write: ChunkedWriter;
|
||||
blob: ReadableStream<Uint8Array>;
|
||||
chunks: ChunkedStream<string>;
|
||||
close(): void;
|
||||
error(err: Error): void;
|
||||
readonly response: Response;
|
||||
}
|
||||
|
||||
export async function createHtmlStream(
|
||||
options: StreamOptions = {},
|
||||
options: StreamOptions = {},
|
||||
): Promise<HtmlStream> {
|
||||
const { chunks, stream } = chunkedHtml();
|
||||
const writer = makeChunkWriter(chunks);
|
||||
const { chunks, stream } = chunkedHtml();
|
||||
const writer = makeChunkWriter(chunks);
|
||||
|
||||
chunks.write(DOCUMENT_TYPE);
|
||||
chunks.write(HTML_BEGIN(options.lang || "en"));
|
||||
options.headContent && chunks.write(options.headContent);
|
||||
chunks.write(HEAD_END);
|
||||
options.bodyAttributes && chunks.write(" " + options.bodyAttributes);
|
||||
chunks.write(BODY_END);
|
||||
chunks.write(DOCUMENT_TYPE);
|
||||
chunks.write(HTML_BEGIN(options.lang || "en"));
|
||||
options.headContent && chunks.write(options.headContent);
|
||||
chunks.write(HEAD_END);
|
||||
options.bodyAttributes && chunks.write(" " + options.bodyAttributes);
|
||||
chunks.write(BODY_END);
|
||||
|
||||
return {
|
||||
write: writer,
|
||||
blob: stream,
|
||||
chunks,
|
||||
close() {
|
||||
if (!chunks.closed) {
|
||||
chunks.write(HTML_END);
|
||||
chunks.close();
|
||||
}
|
||||
},
|
||||
error: chunks.error,
|
||||
response: new Response(stream, {
|
||||
headers: {
|
||||
"Content-Type": "text/html; charset=utf-8",
|
||||
"Transfer-Encoding": "chunked",
|
||||
"Cache-Control": "no-cache",
|
||||
"Connection": "keep-alive",
|
||||
},
|
||||
}),
|
||||
};
|
||||
return {
|
||||
write: writer,
|
||||
blob: stream,
|
||||
chunks,
|
||||
close() {
|
||||
if (!chunks.closed) {
|
||||
chunks.write(HTML_END);
|
||||
chunks.close();
|
||||
}
|
||||
},
|
||||
error: chunks.error,
|
||||
response: new Response(stream, {
|
||||
headers: {
|
||||
"Content-Type": "text/html; charset=utf-8",
|
||||
"Transfer-Encoding": "chunked",
|
||||
"Cache-Control": "no-cache",
|
||||
"Connection": "keep-alive",
|
||||
},
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
|
|
|||
152
src/jsx.ts
152
src/jsx.ts
|
|
@ -12,111 +12,111 @@ type Props = Attrs & { children?: any };
|
|||
type Component = (props: Props) => JsxElement | AsyncGenerator<JsxElement>;
|
||||
|
||||
export type JsxElement =
|
||||
| ((chunks: ChunkedStream<string>) => Promise<void>)
|
||||
| AsyncGenerator<JsxElement, void, unknown>;
|
||||
| ((chunks: ChunkedStream<string>) => Promise<void>)
|
||||
| AsyncGenerator<JsxElement, void, unknown>;
|
||||
|
||||
async function render(
|
||||
child: any,
|
||||
chunks: ChunkedStream<string>,
|
||||
context: ReturnType<typeof html>,
|
||||
child: any,
|
||||
chunks: ChunkedStream<string>,
|
||||
context: ReturnType<typeof html>,
|
||||
): Promise<void> {
|
||||
if (child == null || child === false || child === true) return;
|
||||
if (child == null || child === false || child === true) return;
|
||||
|
||||
if (typeof child === "string") return chunks.write(escape(child));
|
||||
if (typeof child === "number") return chunks.write(String(child));
|
||||
if (typeof child === "string") return chunks.write(escape(child));
|
||||
if (typeof child === "number") return chunks.write(String(child));
|
||||
|
||||
if (typeof child === "function") return await child(chunks);
|
||||
if (child instanceof Promise) {
|
||||
return await render(await child, chunks, context);
|
||||
}
|
||||
if (typeof child === "function") return await child(chunks);
|
||||
if (child instanceof Promise) {
|
||||
return await render(await child, chunks, context);
|
||||
}
|
||||
|
||||
if (typeof child === "object" && Symbol.asyncIterator in child) {
|
||||
(async () => {
|
||||
for await (const item of child) {
|
||||
await render(item, chunks, context);
|
||||
}
|
||||
})();
|
||||
return;
|
||||
}
|
||||
if (typeof child === "object" && Symbol.asyncIterator in child) {
|
||||
(async () => {
|
||||
for await (const item of child) {
|
||||
await render(item, chunks, context);
|
||||
}
|
||||
})();
|
||||
return;
|
||||
}
|
||||
|
||||
if (Array.isArray(child)) {
|
||||
for (const item of child) await render(item, chunks, context);
|
||||
return;
|
||||
}
|
||||
if (Array.isArray(child)) {
|
||||
for (const item of child) await render(item, chunks, context);
|
||||
return;
|
||||
}
|
||||
|
||||
chunks.write(escape(String(child)));
|
||||
chunks.write(escape(String(child)));
|
||||
}
|
||||
|
||||
export function jsx(
|
||||
tag: string | Component | typeof Fragment,
|
||||
props: Props | null = {},
|
||||
tag: string | Component | typeof Fragment,
|
||||
props: Props | null = {},
|
||||
): JsxElement {
|
||||
props ||= {};
|
||||
props ||= {};
|
||||
|
||||
return async (chunks: ChunkedStream<string>) => {
|
||||
const context = html(chunks);
|
||||
const { children, ...attrs } = props;
|
||||
return async (chunks: ChunkedStream<string>) => {
|
||||
const context = html(chunks);
|
||||
const { children, ...attrs } = props;
|
||||
|
||||
if (tag === Fragment) {
|
||||
if (!Array.isArray(children)) {
|
||||
return await render([children], chunks, context);
|
||||
}
|
||||
for (const child of children) {
|
||||
await render(child, chunks, context);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (tag === Fragment) {
|
||||
if (!Array.isArray(children)) {
|
||||
return await render([children], chunks, context);
|
||||
}
|
||||
for (const child of children) {
|
||||
await render(child, chunks, context);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof tag === "function") {
|
||||
return await render(tag(props), chunks, context);
|
||||
}
|
||||
if (typeof tag === "function") {
|
||||
return await render(tag(props), chunks, context);
|
||||
}
|
||||
|
||||
const childr = children == null ? [] : [].concat(children);
|
||||
const attributes = Object.keys(attrs).length ? attrs : {};
|
||||
const childr = children == null ? [] : [].concat(children);
|
||||
const attributes = Object.keys(attrs).length ? attrs : {};
|
||||
|
||||
if (!childr.length || VOID_TAGS.has(tag)) {
|
||||
await context[tag](childr);
|
||||
} else {
|
||||
await context[tag](attributes, async () => {
|
||||
for (const child of childr) {
|
||||
await render(child, chunks, context);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
if (!childr.length || VOID_TAGS.has(tag)) {
|
||||
await context[tag](childr);
|
||||
} else {
|
||||
await context[tag](attributes, async () => {
|
||||
for (const child of childr) {
|
||||
await render(child, chunks, context);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const jsxs = jsx;
|
||||
|
||||
async function renderJsx(
|
||||
element: JsxElement | JsxElement[],
|
||||
chunks: ChunkedStream<string>,
|
||||
element: JsxElement | JsxElement[],
|
||||
chunks: ChunkedStream<string>,
|
||||
): Promise<void> {
|
||||
if (Array.isArray(element)) {
|
||||
for (const el of element) {
|
||||
await renderJsx(el, chunks);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (typeof element === "object" && Symbol.asyncIterator in element) {
|
||||
for await (const item of element) {
|
||||
await renderJsx(item, chunks);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (typeof element === "function") {
|
||||
await element(chunks);
|
||||
}
|
||||
if (Array.isArray(element)) {
|
||||
for (const el of element) {
|
||||
await renderJsx(el, chunks);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (typeof element === "object" && Symbol.asyncIterator in element) {
|
||||
for await (const item of element) {
|
||||
await renderJsx(item, chunks);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (typeof element === "function") {
|
||||
await element(chunks);
|
||||
}
|
||||
}
|
||||
|
||||
export const raw =
|
||||
(html: string): JsxElement => async (chunks: ChunkedStream<string>) =>
|
||||
void (!chunks.closed && chunks.write(html));
|
||||
(html: string): JsxElement => async (chunks: ChunkedStream<string>) =>
|
||||
void (!chunks.closed && chunks.write(html));
|
||||
|
||||
export const open = <K extends keyof HTMLElementTagNameMap>(tag: K) =>
|
||||
raw(`<${tag}>`);
|
||||
raw(`<${tag}>`);
|
||||
|
||||
export const close = <K extends keyof HTMLElementTagNameMap>(tag: K) =>
|
||||
raw(`</${tag}>`);
|
||||
raw(`</${tag}>`);
|
||||
|
||||
export { renderJsx as render };
|
||||
|
|
|
|||
12
src/main.ts
12
src/main.ts
|
|
@ -8,10 +8,10 @@ import App from "./app.tsx";
|
|||
import { render } from "interest/jsx-runtime";
|
||||
|
||||
Deno.serve({
|
||||
port: 8080,
|
||||
async handler() {
|
||||
const stream = await createHtmlStream({ lang: "en" });
|
||||
await render(App(), stream.chunks);
|
||||
return stream.response;
|
||||
},
|
||||
port: 8080,
|
||||
async handler() {
|
||||
const stream = await createHtmlStream({ lang: "en" });
|
||||
await render(App(), stream.chunks);
|
||||
return stream.response;
|
||||
},
|
||||
});
|
||||
|
|
|
|||
208
src/stream.ts
208
src/stream.ts
|
|
@ -4,141 +4,141 @@
|
|||
*/
|
||||
|
||||
export class ChunkedStream<T> implements AsyncIterable<T> {
|
||||
private readonly chunks: T[] = [];
|
||||
private readonly chunks: T[] = [];
|
||||
|
||||
private readonly resolvers: ((result: IteratorResult<T>) => void)[] = [];
|
||||
private readonly rejectors: ((error: Error) => void)[] = [];
|
||||
private readonly resolvers: ((result: IteratorResult<T>) => void)[] = [];
|
||||
private readonly rejectors: ((error: Error) => void)[] = [];
|
||||
|
||||
private _error: Error | null = null;
|
||||
private _closed = false;
|
||||
private _error: Error | null = null;
|
||||
private _closed = false;
|
||||
|
||||
get closed(): boolean {
|
||||
return this._closed;
|
||||
}
|
||||
get closed(): boolean {
|
||||
return this._closed;
|
||||
}
|
||||
|
||||
write(chunk: T) {
|
||||
if (this._closed) throw new Error("Cannot write to closed stream");
|
||||
write(chunk: T) {
|
||||
if (this._closed) throw new Error("Cannot write to closed stream");
|
||||
|
||||
const resolver = this.resolvers.shift();
|
||||
if (resolver) {
|
||||
this.rejectors.shift();
|
||||
resolver({ value: chunk, done: false });
|
||||
} else {
|
||||
this.chunks.push(chunk);
|
||||
}
|
||||
}
|
||||
const resolver = this.resolvers.shift();
|
||||
if (resolver) {
|
||||
this.rejectors.shift();
|
||||
resolver({ value: chunk, done: false });
|
||||
} else {
|
||||
this.chunks.push(chunk);
|
||||
}
|
||||
}
|
||||
|
||||
close(): void {
|
||||
this._closed = true;
|
||||
while (this.resolvers.length) {
|
||||
this.rejectors.shift();
|
||||
this.resolvers.shift()!({ value: undefined! as any, done: true });
|
||||
}
|
||||
}
|
||||
close(): void {
|
||||
this._closed = true;
|
||||
while (this.resolvers.length) {
|
||||
this.rejectors.shift();
|
||||
this.resolvers.shift()!({ value: undefined! as any, done: true });
|
||||
}
|
||||
}
|
||||
|
||||
error(err: Error): void {
|
||||
if (this._closed) return;
|
||||
error(err: Error): void {
|
||||
if (this._closed) return;
|
||||
|
||||
this._error = err;
|
||||
this._closed = true;
|
||||
this._error = err;
|
||||
this._closed = true;
|
||||
|
||||
while (this.rejectors.length) {
|
||||
this.rejectors.shift()!(err);
|
||||
this.resolvers.shift();
|
||||
}
|
||||
}
|
||||
while (this.rejectors.length) {
|
||||
this.rejectors.shift()!(err);
|
||||
this.resolvers.shift();
|
||||
}
|
||||
}
|
||||
|
||||
async next(): Promise<IteratorResult<T>> {
|
||||
if (this._error) {
|
||||
throw this._error;
|
||||
}
|
||||
async next(): Promise<IteratorResult<T>> {
|
||||
if (this._error) {
|
||||
throw this._error;
|
||||
}
|
||||
|
||||
if (this.chunks.length) {
|
||||
return { value: this.chunks.shift()!, done: false };
|
||||
}
|
||||
if (this._closed) return { value: undefined as any, done: true };
|
||||
if (this.chunks.length) {
|
||||
return { value: this.chunks.shift()!, done: false };
|
||||
}
|
||||
if (this._closed) return { value: undefined as any, done: true };
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this.resolvers.push(resolve);
|
||||
this.rejectors.push(reject);
|
||||
});
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
this.resolvers.push(resolve);
|
||||
this.rejectors.push(reject);
|
||||
});
|
||||
}
|
||||
|
||||
[Symbol.asyncIterator](): AsyncIterableIterator<T> {
|
||||
return this;
|
||||
}
|
||||
[Symbol.asyncIterator](): AsyncIterableIterator<T> {
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
export const mapStream = <T, U>(
|
||||
fn: (chunk: T, index: number) => U | Promise<U>,
|
||||
fn: (chunk: T, index: number) => U | Promise<U>,
|
||||
) =>
|
||||
async function* (source: AsyncIterable<T>): AsyncIterable<U> {
|
||||
let index = 0;
|
||||
for await (const chunk of source) yield await fn(chunk, index++);
|
||||
};
|
||||
async function* (source: AsyncIterable<T>): AsyncIterable<U> {
|
||||
let index = 0;
|
||||
for await (const chunk of source) yield await fn(chunk, index++);
|
||||
};
|
||||
|
||||
export const filterStream = <T>(
|
||||
pred: (chunk: T, index: number) => boolean | Promise<boolean>,
|
||||
pred: (chunk: T, index: number) => boolean | Promise<boolean>,
|
||||
) =>
|
||||
async function* (source: AsyncIterable<T>): AsyncIterable<T> {
|
||||
let index = 0;
|
||||
for await (const chunk of source) {
|
||||
if (await pred(chunk, index++)) yield chunk;
|
||||
}
|
||||
};
|
||||
async function* (source: AsyncIterable<T>): AsyncIterable<T> {
|
||||
let index = 0;
|
||||
for await (const chunk of source) {
|
||||
if (await pred(chunk, index++)) yield chunk;
|
||||
}
|
||||
};
|
||||
|
||||
export const takeStream = <T>(count: number) =>
|
||||
async function* (source: AsyncIterable<T>): AsyncIterable<T> {
|
||||
let taken = 0;
|
||||
for await (const chunk of source) {
|
||||
if (taken++ >= count) return;
|
||||
yield chunk;
|
||||
}
|
||||
};
|
||||
async function* (source: AsyncIterable<T>): AsyncIterable<T> {
|
||||
let taken = 0;
|
||||
for await (const chunk of source) {
|
||||
if (taken++ >= count) return;
|
||||
yield chunk;
|
||||
}
|
||||
};
|
||||
|
||||
export const skipStream = <T>(count: number) =>
|
||||
async function* (source: AsyncIterable<T>): AsyncIterable<T> {
|
||||
let index = 0;
|
||||
for await (const chunk of source) {
|
||||
if (index++ >= count) yield chunk;
|
||||
}
|
||||
};
|
||||
async function* (source: AsyncIterable<T>): AsyncIterable<T> {
|
||||
let index = 0;
|
||||
for await (const chunk of source) {
|
||||
if (index++ >= count) yield chunk;
|
||||
}
|
||||
};
|
||||
|
||||
export const batchStream = <T>(size: number) =>
|
||||
async function* (source: AsyncIterable<T>): AsyncIterable<T[]> {
|
||||
let batch: T[] = [];
|
||||
for await (const chunk of source) {
|
||||
batch.push(chunk);
|
||||
if (batch.length >= size) {
|
||||
yield batch;
|
||||
batch = [];
|
||||
}
|
||||
}
|
||||
if (batch.length) yield batch;
|
||||
};
|
||||
async function* (source: AsyncIterable<T>): AsyncIterable<T[]> {
|
||||
let batch: T[] = [];
|
||||
for await (const chunk of source) {
|
||||
batch.push(chunk);
|
||||
if (batch.length >= size) {
|
||||
yield batch;
|
||||
batch = [];
|
||||
}
|
||||
}
|
||||
if (batch.length) yield batch;
|
||||
};
|
||||
|
||||
export const tapStream = <T>(
|
||||
fn: (chunk: T, index: number) => void | Promise<void>,
|
||||
fn: (chunk: T, index: number) => void | Promise<void>,
|
||||
) =>
|
||||
async function* (source: AsyncIterable<T>): AsyncIterable<T> {
|
||||
let index = 0;
|
||||
for await (const chunk of source) {
|
||||
yield chunk;
|
||||
await fn(chunk, index++);
|
||||
}
|
||||
};
|
||||
async function* (source: AsyncIterable<T>): AsyncIterable<T> {
|
||||
let index = 0;
|
||||
for await (const chunk of source) {
|
||||
yield chunk;
|
||||
await fn(chunk, index++);
|
||||
}
|
||||
};
|
||||
|
||||
export const catchStream = <T>(
|
||||
handler: (error: Error) => void | Promise<void>,
|
||||
handler: (error: Error) => void | Promise<void>,
|
||||
) =>
|
||||
async function* (source: AsyncIterable<T>): AsyncIterable<T> {
|
||||
try {
|
||||
for await (const chunk of source) yield chunk;
|
||||
} catch (err) {
|
||||
await handler(err instanceof Error ? err : new Error(String(err)));
|
||||
}
|
||||
};
|
||||
async function* (source: AsyncIterable<T>): AsyncIterable<T> {
|
||||
try {
|
||||
for await (const chunk of source) yield chunk;
|
||||
} catch (err) {
|
||||
await handler(err instanceof Error ? err : new Error(String(err)));
|
||||
}
|
||||
};
|
||||
|
||||
export const pipe =
|
||||
<T>(...fns: Array<(src: AsyncIterable<T>) => AsyncIterable<any>>) =>
|
||||
(source: AsyncIterable<T>) => fns.reduce((acc, fn) => fn(acc), source);
|
||||
<T>(...fns: Array<(src: AsyncIterable<T>) => AsyncIterable<any>>) =>
|
||||
(source: AsyncIterable<T>) => fns.reduce((acc, fn) => fn(acc), source);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue