Skip to content

Commit

Permalink
feat: Render TSX from functions
Browse files Browse the repository at this point in the history
  • Loading branch information
ericselin committed Feb 19, 2022
1 parent 7a679d7 commit 2d6e3e1
Show file tree
Hide file tree
Showing 9 changed files with 175 additions and 69 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),

Now you can specify a generator that yields content files in a `bob.ts` configuration file. This generator is run with the `--import` CLI flag. Partial updates based on the last import date are also possible. See the docs.

- Render TSX from functions

Use the function context `renderResponse` to render a TSX component as the response. This method comes with type checking of the component and subsequent arguments. Just pass in a normal component and the props you want!

## 2.3.0 - 2022-02-03

### Fixed
Expand Down
13 changes: 11 additions & 2 deletions cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
serve as serveFunctions,
writeNginxLocations,
} from "./functions/mod.ts";
import type { Functions } from "./functions/mod.ts";
import { createChangesApplier } from "./core/api.ts";
import changeOnFileModifications from "./core/change-providers/fs-mod.ts";
import { FileCache } from "./core/cache.ts";
Expand Down Expand Up @@ -145,21 +146,29 @@ const buildOptions: BuildOptions = {
log,
};

let functionDefinitions: Functions | undefined = undefined;

// try to load config
const configModule = await loadIfExists("bob.ts");
const configFile: ConfigFile | undefined = configModule?.default;
if (configFile) {
log.info("Using config file `bob.ts`");
// import content if we have an importer and CLI flag set
if (configFile.contentImporter && args.import) {
await importContent(configFile.contentImporter, buildOptions.contentDir, buildOptions.cache, log);
await importContent(
configFile.contentImporter,
buildOptions.contentDir,
buildOptions.cache,
log,
);
}
functionDefinitions = configFile.functions;
}

const functionsPort = 8081;

if (functions || server) {
await serveFunctions({ log, buildOptions });
await serveFunctions({ log, buildOptions, functionDefinitions });
}

if (args.fnNginxConf) {
Expand Down
49 changes: 28 additions & 21 deletions core/jsx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ or email [email protected] <mailto:[email protected]>
*/

import type {
Component,
Context,
Element,
ElementCreator,
Expand All @@ -42,8 +43,8 @@ export const h: ElementCreator = (type, props, ...children) => {
return element;
};

const renderProps = (props?: Props): string => {
if (!props) return "";
const renderProps = (props?: unknown): string => {
if (!props || typeof props !== "object") return "";
return Object.entries(props).reduce(
(all, [attr, value]) => `${all} ${attr}="${value}"`,
"",
Expand All @@ -63,36 +64,36 @@ export const createRenderer: ElementRendererCreator = (options, getPages) =>
const renderContext = {
needsCss: [] as string[],
};
const render: ElementRenderer = async (component) => {
const render: ElementRenderer = async (element) => {
let html = "";

component = await component;
element = await element;

// if this is an array, render recursively
if (Array.isArray(component)) {
for (const c of component) {
if (Array.isArray(element)) {
for (const c of element) {
html += await render(c);
}
return html;
}

if (component === null) {
if (element === null) {
return "";
}

switch (typeof component) {
switch (typeof element) {
case "undefined":
return "";
case "string":
return component;
return element;
case "number":
case "boolean":
return component.toString();
return element.toString();
}

// see if this is an html tag
if (typeof component.type === "string") {
const { type, props, children } = component;
if (typeof element.type === "string") {
const { type, props, children } = element;

html += `<${type}${renderProps(props)}>`;

Expand Down Expand Up @@ -120,15 +121,15 @@ export const createRenderer: ElementRendererCreator = (options, getPages) =>

// if we get here, the element should be an actual renderable jsx component

const props = component.props || {};
props.children = component.children;
if (component.needsCss) {
const props = element.props as Props || {};
props.children = element.children;
if (element.needsCss) {
renderContext.needsCss = [
...renderContext.needsCss,
path.join(options.layoutDir, component.needsCss),
path.join(options.layoutDir, element.needsCss),
];
}
const context: Context = {
const context: Context<unknown, unknown, unknown> = {
page: contentPage as Page,
needsCss: renderContext.needsCss,
get childPages() {
Expand All @@ -138,10 +139,10 @@ export const createRenderer: ElementRendererCreator = (options, getPages) =>
return undefined;
},
};
if (component.wantsPages) {
if (element.wantsPages) {
context.wantedPages = getPages &&
// get all wanted pages
await getPages(component.wantsPages)
await getPages(element.wantsPages)
.then((wantedPages) =>
// filter out the current page from wanted pages
wantedPages.filter((page) =>
Expand All @@ -152,8 +153,14 @@ export const createRenderer: ElementRendererCreator = (options, getPages) =>
}

try {
component = await component.type(props, context);
return render(component);
const component = element.type as Component<
unknown,
unknown,
unknown,
unknown
>;
element = await component(props, context);
return render(element);
} catch (e) {
options.log?.error(
`Error rendering page ${contentPage?.location.inputPath}`,
Expand Down
8 changes: 1 addition & 7 deletions core/layout-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ or email [email protected] <mailto:[email protected]>
import type {
BuildOptions,
Component,
DefaultProps,
LayoutLoader,
PagesGetter,
RenderablePage,
Expand Down Expand Up @@ -125,12 +124,7 @@ const loadLayout = (
renderer: (content) =>
renderJsx(content)(
h(
layout.module.default as Component<
DefaultProps,
unknown,
unknown,
unknown
>,
layout.module.default as Component,
),
),
};
Expand Down
6 changes: 6 additions & 0 deletions docs/content/docs/functions-jsx/bob.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import type { ConfigFile } from "../../../../mod.ts";
import functions from "./functions.tsx";

export default <ConfigFile> {
functions,
};
30 changes: 30 additions & 0 deletions docs/content/docs/functions-jsx/functions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/** @jsx h */

import { Component, FunctionHandler, Functions, h } from "../../../../mod.ts";

const Base: Component = (props, { page }) => (
<html>
<head>
<title>{page.title || "Hello world"}</title>
</head>
<body>
{props.children}
</body>
</html>
);

const Root: Component<{ greetings: string[] }> = ({ greetings }) => (
<Base>
Greetings to: {greetings.length ? greetings.join() : "Nobody :("}
</Base>
);

const rootHandler: FunctionHandler = (_req, ctx) => {
return ctx.renderResponse(Root, { greetings: ["moi"] });
};

const functions: Functions = [
["/", rootHandler],
];

export default functions;
9 changes: 9 additions & 0 deletions docs/content/docs/functions-jsx/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
title: JSX in functions
---

You can render JSX (or maybe more technically TSX) in functions. Just create your JSX component as a normal `Component` and call the `renderResponse` function available in the context.

## Example

code:functions.
36 changes: 21 additions & 15 deletions domain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,11 @@ Configuration file
*/

import { Functions } from "./functions/mod.ts";

export type ConfigFile = {
contentImporter: ContentImporter;
contentImporter?: ContentImporter;
functions?: Functions;
};

/*
Expand All @@ -38,13 +41,13 @@ TSX Components

/** Component type to be used for all TSX components, both layouts and standalone. */
export type Component<
P extends Props = DefaultProps,
PageFrontmatter = unknown,
Props = unknown,
PageFrontmatter = undefined,
WantedPagesFrontmatter = undefined,
ChildPagesFrontmatter = undefined,
> =
& ((
props: P & { children?: Children },
props: Props & { children?: Children },
context: Context<
PageFrontmatter,
WantedPagesFrontmatter,
Expand All @@ -61,8 +64,8 @@ export type AnyComponent = Component<DefaultProps, unknown, unknown, unknown>;
/** Context for rendering a specific page. */
export type Context<
PageFrontmatter = unknown,
WantedPagesFrontmatter = unknown,
ChildPagesFrontmatter = unknown,
WantedPagesFrontmatter = undefined,
ChildPagesFrontmatter = undefined,
> = {
page: Page<PageFrontmatter>;
needsCss: CssSpecifier[];
Expand All @@ -72,11 +75,6 @@ export type Context<
: ChildPages<ChildPagesFrontmatter>;
};

/** Component properties. */
export type Props = Record<string, unknown>;
/** Default component properties. */
export type DefaultProps = Props;

type WantsPages = string | string[];
type WantedPages<W = unknown> = Page<W>[];
type ChildPages<W = unknown> = Promise<Page<W>[]>;
Expand All @@ -91,7 +89,7 @@ TSX rendering
*/

/** An `Element` is the internal representation of a `Component` or other node, such as a string. */
export type Element<P extends Props = DefaultProps> = {
export type Element<P = unknown> = {
type: ElementType;
props?: P;
children?: Children[];
Expand All @@ -102,7 +100,7 @@ export type Element<P extends Props = DefaultProps> = {
/** Creates elements from transpiled JSX calls. This is equivalent to `React.createElement` or `Preact.h`. */
export type ElementCreator = (
type: ElementType,
props?: Props,
props?: unknown,
...children: Children[]
) => Element;

Expand All @@ -122,11 +120,17 @@ export type ElementRendererCreator = (
/** Get an array of the pages wanted by a layout or a content page. */
export type PagesGetter = (wantsPages: WantsPages) => Promise<Page[]>;

type ElementType = Component<DefaultProps, unknown, unknown, unknown> | string;
type ElementType = Component<undefined> | string;

type Child = Element | string | number | boolean | null | undefined;
type Children = Child | Child[];

/** Component properties. */
export type Props = Record<string, unknown>;

/** Default component properties. */
type DefaultProps = Props;

export const HTMLEmptyElements = [
"area",
"base",
Expand Down Expand Up @@ -260,7 +264,9 @@ type OutputChange = {
outputPath: CwdRelativePath;
};

export type ContentImporter = AsyncGenerator<ImportedContent | DeletedContent | undefined>
export type ContentImporter = AsyncGenerator<
ImportedContent | DeletedContent | undefined
>;

export type ImportedContent<T = unknown> = {
contentPath: string;
Expand Down
Loading

0 comments on commit 2d6e3e1

Please sign in to comment.