;
37 | }
38 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Flareact
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/docs/built-in-css-support.md:
--------------------------------------------------------------------------------
1 | # Built-In CSS Support
2 |
3 | Flareact currently supports **standard CSS imports**:
4 |
5 | ```js
6 | import '../styles.css';
7 |
8 | export default MyComponent() {
9 | //
10 | }
11 | ```
12 |
13 | Your compiled stylesheet will be injected into your document. When deploying, your styles will be minimized automatically.
14 |
15 | A few warning about CSS support in Flareact:
16 |
17 | - No support yet for Sass/Less/etc
18 | - No support for scoped CSS Modules
19 | - In production, Flareact will always attempt to load a `main.css` stylesheet, even if you don't have styles defined. _lol, sorry._
20 |
21 | ## Global Styles
22 |
23 | To import a global stylesheet in your application, create a [custom App page](/docs/custom-app-page) and import the style there.
24 |
25 | In `pages/_app.js`:
26 |
27 | ```js
28 | import "../styles.css";
29 |
30 | export default function App({ Component, pageProps }) {
31 | return ;
32 | }
33 | ```
34 |
35 | ## PostCSS
36 |
37 | Flareact processes all styles through [PostCSS](https://postcss.org/).
38 |
39 | [Learn more about customizing your PostCSS config](/docs/custom-postcss-config).
40 |
--------------------------------------------------------------------------------
/docs/custom-postcss-config.md:
--------------------------------------------------------------------------------
1 | # Custom PostCSS Config
2 |
3 | Flareact processes your CSS using PostCSS. By default, Flareact uses the following plugins:
4 |
5 | ```js
6 | module.exports = {
7 | plugins: [
8 | require("postcss-flexbugs-fixes"),
9 | require("postcss-preset-env")({
10 | autoprefixer: {
11 | flexbox: "no-2009",
12 | },
13 | stage: 3,
14 | features: {
15 | "custom-properties": false,
16 | },
17 | }),
18 | ],
19 | };
20 | ```
21 |
22 | If you need to customize the PostCSS plugins for your project, you can define a local `postcss.config.js` file.
23 |
24 | **Note**: If you define a custom PostCSS config, it will completely replace the config that Flareact provides - so be sure to include everything you need to process your styles.
25 |
26 | Here's an example for using TailwindCSS:
27 |
28 | ```js
29 | module.exports = {
30 | plugins: [
31 | require("postcss-flexbugs-fixes"),
32 | require("postcss-preset-env")({
33 | autoprefixer: {
34 | flexbox: "no-2009",
35 | },
36 | stage: 3,
37 | features: {
38 | "custom-properties": false,
39 | },
40 | }),
41 | ],
42 | };
43 | ```
44 |
--------------------------------------------------------------------------------
/src/client/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom";
3 | import PageLoader from "./page-loader";
4 | import { RouterProvider } from "../router";
5 | import AppProvider from "../components/AppProvider";
6 |
7 | const initialData = JSON.parse(
8 | document.getElementById("__FLAREACT_DATA").textContent
9 | );
10 |
11 | window.__FLAREACT_DATA = initialData;
12 |
13 | const pagePath = initialData.page.pagePath;
14 | const pageLoader = new PageLoader(pagePath);
15 |
16 | const register = (page) => pageLoader.registerPage(page);
17 |
18 | if (window.__FLAREACT_PAGES) {
19 | window.__FLAREACT_PAGES.forEach((p) => register(p));
20 | }
21 |
22 | window.__FLAREACT_PAGES = [];
23 | window.__FLAREACT_PAGES.push = register;
24 |
25 | async function render() {
26 | const App = await pageLoader.loadPage("/_app");
27 | const Component = await pageLoader.loadPage(pagePath);
28 |
29 | ReactDOM.hydrate(
30 |
36 |
41 | ,
42 | document.getElementById("__flareact")
43 | );
44 | }
45 |
46 | render();
47 |
--------------------------------------------------------------------------------
/src/utils.js:
--------------------------------------------------------------------------------
1 | import { DYNAMIC_PAGE } from "./worker/pages";
2 |
3 | /**
4 | * Extract dynamic params from a slug path. For use in the router, but should eventually
5 | * be refactored to share responsibilities with the /worker/pages.js module.
6 | *
7 | * @param {string} source e.g. /articles/[slug]
8 | * @param {string} path e.g. /articles/hello-world
9 | * @returns {<[string]: string>} e.g. { slug: 'hello-world' }
10 | */
11 | export function extractDynamicParams(source, path) {
12 | let test = source;
13 | let parts = [];
14 | let params = {};
15 |
16 | for (const match of source.matchAll(/\[(\w+)\]/g)) {
17 | parts.push(match[1]);
18 |
19 | test = test.replace(DYNAMIC_PAGE, () => "([\\w_-]+)");
20 | }
21 |
22 | test = new RegExp(test, "g");
23 |
24 | const matches = path.matchAll(test);
25 |
26 | for (const match of matches) {
27 | parts.forEach((part, idx) => (params[part] = match[idx + 1]));
28 | }
29 |
30 | return params;
31 | }
32 |
33 | // This utility is based on https://github.com/zertosh/htmlescape
34 | // License: https://github.com/zertosh/htmlescape/blob/0527ca7156a524d256101bb310a9f970f63078ad/LICENSE
35 |
36 | const ESCAPE_LOOKUP = {
37 | "&": "\\u0026",
38 | ">": "\\u003e",
39 | "<": "\\u003c",
40 | "\u2028": "\\u2028",
41 | "\u2029": "\\u2029",
42 | };
43 |
44 | const ESCAPE_REGEX = /[&><\u2028\u2029]/g;
45 |
46 | export function htmlEscapeJsonString(str) {
47 | return str.replace(ESCAPE_REGEX, (match) => ESCAPE_LOOKUP[match]);
48 | }
49 |
--------------------------------------------------------------------------------
/docs/dynamic-routes.md:
--------------------------------------------------------------------------------
1 | # Dynamic Routes
2 |
3 | Often times, your app pages will need to be created dynamically.
4 |
5 | Similar to page-based routes, Flareact allows you to define **dynamic routes** using React components defined in files.
6 |
7 | To define a dynamic route, create a file with a name wrapped by square brackets `[]`, e.g. `/pages/[slug].js`.
8 |
9 | ## Dynamic Routes with `getEdgeProps`
10 |
11 | If you need to [fetch data](/docs/data-fetching) for your dynamic page component, the dynamic parts will be passed as a `params` property to your `getEdgeProps` function.
12 |
13 | **Example**: Your `/pages/posts/[slug].js` file might look like this:
14 |
15 | ```js
16 | export async function getEdgeProps({ params }) {
17 | const { slug } = params;
18 | const post = await getSomeRemotePost({ slug });
19 |
20 | return {
21 | props: {
22 | post
23 | }
24 | }
25 | }
26 |
27 | export default function Post({ post }) {
28 | ...
29 | }
30 | ```
31 |
32 | ## Nested Dynamic Routes
33 |
34 | You can also nest dynamic routes, e.g.
35 |
36 | ```
37 | /pages/posts/[category]/[slug].js
38 | ```
39 |
40 | The params passed to your `getEdgeProps` function will contain each dynamic path property:
41 |
42 | ```js
43 | {
44 | params: {
45 | category,
46 | slug,
47 | }
48 | }
49 | ```
50 |
51 | You can also reference the query params with the `useRouter` hook in your component:
52 |
53 | ```js
54 | function Post() {
55 | const router = useRouter();
56 |
57 | const { category, slug } = router.query;
58 | }
59 | ```
60 |
61 | ## Resources
62 |
63 | - [Linking to Dynamic Routes](/docs/flareact-link#dynamic-routes)
64 |
--------------------------------------------------------------------------------
/docs/custom-webpack-config.md:
--------------------------------------------------------------------------------
1 | # Custom Webpack Config
2 |
3 | Flareact allows you to customize the Webpack config for your worker and client builds.
4 |
5 | To modify the Webpack config, define a `flareact.config.js` file in the root of your project:
6 |
7 | ```js
8 | module.exports = {
9 | webpack: (config, { dev, isWorker, defaultLoaders, webpack }) => {
10 | // Note: we provide webpack above so you should not `require` it
11 | // Perform customizations to webpack config
12 | config.plugins.push(new webpack.IgnorePlugin(/\/__tests__\//));
13 |
14 | // Important: return the modified config
15 | return config;
16 | },
17 | };
18 | ```
19 |
20 | The `webpack` function is executed twice, once for the worker and once for the client. This allows you to distinguish between client and server configuration using the `isWorker` property.
21 |
22 | The second argument to the `webpack` function is an object with the following properties:
23 |
24 | - `dev`: Boolean - Indicates if the compilation will be done in development
25 | - `isWorker`: Boolean - It's true for worker-side compilation, and false for client-side compilation
26 | - `defaultLoaders`: Object - Default loaders used internally by Flareact:
27 | - `babel`: Object - Default babel-loader configuration
28 |
29 | Example usage of `defaultLoaders.babel`:
30 |
31 | ```js
32 | module.exports = {
33 | webpack: (config, options) => {
34 | config.module.rules.push({
35 | test: /\.mdx/,
36 | use: [
37 | options.defaultLoaders.babel,
38 | {
39 | loader: "@mdx-js/loader",
40 | options: {},
41 | },
42 | ],
43 | });
44 |
45 | return config;
46 | },
47 | };
48 | ```
49 |
--------------------------------------------------------------------------------
/docs/flareact-link.md:
--------------------------------------------------------------------------------
1 | # flareact/link
2 |
3 | To perform client-side routing between different pages of your Flareact app, use the `flareact/link` component:
4 |
5 | ```js
6 | import Link from "flareact/link";
7 |
8 | export default function Index() {
9 | return (
10 |
58 | );
59 | }
60 | ```
61 |
--------------------------------------------------------------------------------
/configs/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require("path");
2 | const MiniCssExtractPlugin = require("mini-css-extract-plugin");
3 | const defaultLoaders = require("./loaders");
4 | const { fileExistsInDir } = require("./utils");
5 |
6 | module.exports = function ({ dev, isServer }) {
7 | const loaders = defaultLoaders({ dev, isServer });
8 |
9 | const cssExtractLoader = {
10 | loader: MiniCssExtractPlugin.loader,
11 | };
12 |
13 | const styleLoader = "style-loader";
14 |
15 | const finalStyleLoader = () => {
16 | if (dev) {
17 | if (isServer) return cssExtractLoader;
18 | return styleLoader;
19 | } else {
20 | return cssExtractLoader;
21 | }
22 | };
23 |
24 | return {
25 | context: process.cwd(),
26 | plugins: [new MiniCssExtractPlugin()],
27 | stats: "errors-warnings",
28 | module: {
29 | rules: [
30 | {
31 | test: /\.js$/,
32 | exclude: /node_modules\/(?!(flareact)\/).*/,
33 | use: loaders.babel,
34 | },
35 | {
36 | test: /\.css$/,
37 | use: isServer
38 | ? require.resolve("null-loader")
39 | : [
40 | finalStyleLoader(),
41 | { loader: "css-loader", options: { importLoaders: 1 } },
42 | {
43 | loader: "postcss-loader",
44 | options: {
45 | config: {
46 | path: fileExistsInDir(process.cwd(), "postcss.config.js")
47 | ? process.cwd()
48 | : path.resolve(__dirname),
49 | },
50 | },
51 | },
52 | ],
53 | },
54 | ],
55 | },
56 | };
57 | };
58 |
--------------------------------------------------------------------------------
/configs/webpack.worker.config.js:
--------------------------------------------------------------------------------
1 | const baseConfig = require("./webpack.config");
2 | const CopyPlugin = require("copy-webpack-plugin");
3 | const path = require("path");
4 | const webpack = require("webpack");
5 | const { flareactConfig } = require("./utils");
6 | const defaultLoaders = require("./loaders");
7 | const { nanoid } = require("nanoid");
8 | const fs = require("fs");
9 |
10 | const dev = !!process.env.WORKER_DEV;
11 | const isServer = true;
12 | const projectDir = process.cwd();
13 | const flareact = flareactConfig(projectDir);
14 |
15 | const outPath = path.resolve(projectDir, "out");
16 |
17 | const buildManifest = dev
18 | ? {}
19 | : fs.readFileSync(
20 | path.join(outPath, "_flareact", "static", "build-manifest.json"),
21 | "utf-8"
22 | );
23 |
24 | module.exports = function (env, argv) {
25 | let config = {
26 | ...baseConfig({ dev, isServer }),
27 | target: "webworker",
28 | entry: path.resolve(projectDir, "./index.js"),
29 | };
30 |
31 | config.plugins.push(
32 | new CopyPlugin([
33 | {
34 | from: path.resolve(projectDir, "public"),
35 | to: outPath,
36 | },
37 | ])
38 | );
39 |
40 | let inlineVars = {
41 | "process.env.BUILD_ID": JSON.stringify(nanoid()),
42 | };
43 |
44 | if (dev) {
45 | inlineVars.DEV = dev;
46 | } else {
47 | inlineVars["process.env.BUILD_MANIFEST"] = buildManifest;
48 | }
49 |
50 | config.plugins.push(new webpack.DefinePlugin(inlineVars));
51 |
52 | if (flareact.webpack) {
53 | config = flareact.webpack(config, {
54 | dev,
55 | isServer,
56 | isWorker: isServer,
57 | defaultLoaders: defaultLoaders({ dev, isServer }),
58 | webpack,
59 | });
60 | }
61 |
62 | return config;
63 | };
64 |
--------------------------------------------------------------------------------
/docs/api-routes.md:
--------------------------------------------------------------------------------
1 | # API Routes
2 |
3 | Flareact provides a way for you to build your **API** using the existing file-based routing system.
4 |
5 | Any file inside the folder `pages/api` is mapped to `/api/*` and will be treated as an API endpoint instead of a `page`.
6 |
7 | For example, the following API route `pages/api/hello.js` returns a standard text response:
8 |
9 | ```js
10 | export default async (event) => {
11 | return new Response("Hello there");
12 | };
13 | ```
14 |
15 | For an API route to work, you need to export a default function (a **request handler**) which receives a [`FetchEvent` parameter](https://developers.cloudflare.com/workers/reference/apis/fetch-event).
16 |
17 | Your API method may return a new instance of [`Response`](https://developers.cloudflare.com/workers/reference/apis/response/). This allows you to customize headers, formatting, and status code.
18 |
19 | However, Flareact will conveniently wrap your API method's response in a `Response` object if you return only a primitive:
20 |
21 | ```js
22 | export default async (event) => {
23 | return { hello: "world" };
24 | };
25 |
26 | // becomes:
27 | // new Response(JSON.stringify({ hello: 'world' }), { headers: { 'content-type': 'application/json' }})
28 |
29 | export default async (event) => {
30 | return "Hello, world";
31 | };
32 |
33 | // becomes:
34 | // new Response('Hello, world')
35 | ```
36 |
37 | API routes handle requests exactly like [standard Cloudflare Worker requests](https://developers.cloudflare.com/workers/about/how-it-works/), except that you **do not need to call `event.respondWith`**.
38 |
39 | **Note**: You can use `fetch` natively within `getEdgeProps` without needing to require any polyfills, because it is a first-class WebWorker API supported by Workers.
40 |
--------------------------------------------------------------------------------
/docs/comparison-to-nextjs.md:
--------------------------------------------------------------------------------
1 | # Comparison to Next.js
2 |
3 | Flareact is modeled closely after [Next.js](https://nextjs.org). Here are a few key differences:
4 |
5 | - Next.js emphasizes static generation, while Flareact leverages edge-computing + caching.
6 | - Next.js has lots of optimizations built in, and it's considered more "production ready"
7 | - Lots of other things 😊
8 |
9 | ## Rendering Modes
10 |
11 | Next.js offers three distinct ways to render your pages:
12 |
13 | - **Static-Site Generation (SSG)**: Your pages are generated at deploy time with `getStaticProps`. They are optionally revalidated on a timed basis to support **incremental re-generation** of your pages (useful for e.g. pulling in your latest blog posts).
14 | - **Server-Side Rendering (SSR)**: Your pages are generated on-demand with each request with `getServerProps`. This is less common, given the powerful tool of incremental SSG above.
15 | - **Client-Side Rendering (CSR)**: If you don't need to have your data fetched as part of your initial HTML payload, you can fetch it within your component as a typical AJAX request.
16 |
17 | Flareact offers a similar approach with **Edge-Side Rendering (ESR)**:
18 |
19 | - Your pages are generated with `getEdgeProps` and cached using the [Cloudflare Worker Cache](https://developers.cloudflare.com/workers/reference/apis/cache/) by default at the edge, similar to **SSG**.
20 | - Optionally, your pages can be revalidated after a specified time, similar to **incremental SSG**.
21 | - If want, pages can also be revalidated on every single request, similar to **SSR**.
22 | - **Client-Side Rendering (CSR)**: If you don't need to have your data fetched as part of your initial HTML payload, you can fetch it within your component as a typical AJAX request.
23 |
24 | [Learn more about data fetching in Flareact](/docs/data-fetching)
25 |
--------------------------------------------------------------------------------
/tests/pages.spec.js:
--------------------------------------------------------------------------------
1 | import { resolvePagePath } from "../src/worker/pages";
2 |
3 | it("matches simple pages", () => {
4 | const path = resolvePagePath("/index", [
5 | "./index.js",
6 | "./apples.js",
7 | "./posts/[slug].js",
8 | ]);
9 |
10 | expect(path).toBeTruthy();
11 | expect(path.page).toBe("./index.js");
12 | });
13 |
14 | it("matches dynamic pages", () => {
15 | const path = resolvePagePath("/posts/hello", [
16 | "./index.js",
17 | "./apples.js",
18 | "./posts/[slug].js",
19 | ]);
20 |
21 | expect(path).toBeTruthy();
22 | expect(path.page).toBe("./posts/[slug].js");
23 | expect(path.params).toEqual({ slug: "hello" });
24 | });
25 |
26 | it("matches dynamic page indexes", () => {
27 | const path = resolvePagePath("/posts", [
28 | "./index.js",
29 | "./apples.js",
30 | "./posts/[slug].js",
31 | "./posts/index.js",
32 | ]);
33 |
34 | expect(path).toBeTruthy();
35 | expect(path.page).toBe("./posts/index.js");
36 | });
37 |
38 | it("matches dynamic page indexes matching directory names", () => {
39 | const path = resolvePagePath("/posts", [
40 | "./index.js",
41 | "./apples.js",
42 | "./posts/[slug].js",
43 | "./posts.js",
44 | ]);
45 |
46 | expect(path).toBeTruthy();
47 | expect(path.page).toBe("./posts.js");
48 | });
49 |
50 | it("matches longer dynamic pages", () => {
51 | const path = resolvePagePath("/posts/hello-world-it-me", [
52 | "./index.js",
53 | "./apples.js",
54 | "./posts/[slug].js",
55 | ]);
56 |
57 | expect(path).toBeTruthy();
58 | expect(path.page).toBe("./posts/[slug].js");
59 | expect(path.params).toEqual({ slug: "hello-world-it-me" });
60 | });
61 |
62 | it("matches multiple dynamic pages", () => {
63 | const path = resolvePagePath("/posts/travel/hello-world-it-me", [
64 | "./index.js",
65 | "./apples.js",
66 | "./posts/[category]/[slug].js",
67 | ]);
68 |
69 | expect(path).toBeTruthy();
70 | expect(path.page).toBe("./posts/[category]/[slug].js");
71 | expect(path.params).toEqual({
72 | category: "travel",
73 | slug: "hello-world-it-me",
74 | });
75 | });
76 |
--------------------------------------------------------------------------------
/src/worker/worker.js:
--------------------------------------------------------------------------------
1 | import { handleRequest } from ".";
2 | import {
3 | getAssetFromKV,
4 | mapRequestToAsset,
5 | } from "@cloudflare/kv-asset-handler";
6 |
7 | export async function handleEvent(event, context, DEBUG) {
8 | let options = {};
9 |
10 | /**
11 | * You can add custom logic to how we fetch your assets
12 | * by configuring the function `mapRequestToAsset`
13 | */
14 | // options.mapRequestToAsset = handlePrefix(/^\/docs/)
15 |
16 | return await handleRequest(event, context, async () => {
17 | try {
18 | options.cacheControl = {
19 | // By default, cache static assets for one year
20 | browserTTL: 365 * 24 * 60 * 60,
21 | };
22 |
23 | if (DEBUG) {
24 | // customize caching
25 | options.cacheControl.bypassCache = true;
26 | }
27 |
28 | return await getAssetFromKV(event, options);
29 | } catch (e) {
30 | // if an error is thrown try to serve the asset at 404.html
31 | if (!DEBUG) {
32 | try {
33 | let notFoundResponse = await getAssetFromKV(event, {
34 | mapRequestToAsset: (req) =>
35 | new Request(`${new URL(req.url).origin}/404.html`, req),
36 | });
37 |
38 | return new Response(notFoundResponse.body, {
39 | ...notFoundResponse,
40 | status: 404,
41 | });
42 | } catch (e) {}
43 | }
44 |
45 | return new Response(e.message || e.toString(), { status: 500 });
46 | }
47 | });
48 | }
49 |
50 | /**
51 | * Here's one example of how to modify a request to
52 | * remove a specific prefix, in this case `/docs` from
53 | * the url. This can be useful if you are deploying to a
54 | * route on a zone, or if you only want your static content
55 | * to exist at a specific path.
56 | */
57 | function handlePrefix(prefix) {
58 | return (request) => {
59 | // compute the default (e.g. / -> index.html)
60 | let defaultAssetKey = mapRequestToAsset(request);
61 | let url = new URL(defaultAssetKey.url);
62 |
63 | // strip the prefix from the path for lookup
64 | url.pathname = url.pathname.replace(prefix, "/");
65 |
66 | // inherit all other props from the default request
67 | return new Request(url.toString(), defaultAssetKey);
68 | };
69 | }
70 |
--------------------------------------------------------------------------------
/configs/webpack/plugins/build-manifest-plugin.js:
--------------------------------------------------------------------------------
1 | const { RawSource } = require("webpack-sources");
2 |
3 | module.exports = class BuildManifestPlugin {
4 | createAssets(compilation, assets) {
5 | const namedChunks = compilation.namedChunks;
6 |
7 | const assetMap = {
8 | helpers: ["_buildManifest.js"],
9 | pages: {},
10 | };
11 |
12 | const mainJsChunk = namedChunks.get("main");
13 | const mainJsFiles = [...mainJsChunk.files].filter((file) =>
14 | file.endsWith(".js")
15 | );
16 |
17 | for (const entrypoint of compilation.entrypoints.values()) {
18 | let pagePath = entrypoint.name.match(/^pages[/\\](.*)$/);
19 |
20 | if (!pagePath) continue;
21 |
22 | const pageFiles = [...entrypoint.getFiles()].filter(
23 | (file) => file.endsWith(".css") || file.endsWith(".js")
24 | );
25 |
26 | let pageName = pagePath[1];
27 |
28 | // Flatten any dynamic `index` pages
29 | pageName = pageName.replace(/\/index/, "");
30 |
31 | assetMap.pages[`/${pageName}`] = [...mainJsFiles, ...pageFiles];
32 | }
33 |
34 | assets["build-manifest.json"] = new RawSource(
35 | JSON.stringify(assetMap, null, 2)
36 | );
37 |
38 | assets["_buildManifest.js"] = new RawSource(
39 | `self.__BUILD_MANIFEST = ${generateClientManifest(
40 | assetMap
41 | )};self.__BUILD_MANIFEST_CB && self.__BUILD_MANIFEST_CB();`
42 | );
43 |
44 | return assets;
45 | }
46 |
47 | apply(compiler) {
48 | compiler.hooks.emit.tap("FlareactBuildManifest", (compilation) => {
49 | this.createAssets(compilation, compilation.assets);
50 | });
51 | }
52 | };
53 |
54 | /**
55 | * Take an asset map and generate a client version with just pages to be used for
56 | * client page routing, loading and transitions.
57 | *
58 | * @param {object} assetMap
59 | */
60 | function generateClientManifest(assetMap) {
61 | let clientManifest = {};
62 | const appDependencies = new Set(assetMap.pages["/_app"]);
63 |
64 | Object.entries(assetMap.pages).forEach(([page, files]) => {
65 | if (page === "/_app") return;
66 |
67 | const filteredDeps = files.filter((file) => !appDependencies.has(file));
68 |
69 | clientManifest[page] = filteredDeps;
70 | });
71 |
72 | return JSON.stringify(clientManifest);
73 | }
74 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "flareact",
3 | "version": "0.0.0-development",
4 | "main": "src/index.js",
5 | "module": "src/index.js",
6 | "description": "Edge-Rendered React framework built for Cloudflare Workers",
7 | "files": [
8 | "src",
9 | "configs",
10 | "link.js",
11 | "head.js",
12 | "router.js",
13 | "webpack.js"
14 | ],
15 | "license": "MIT",
16 | "scripts": {
17 | "test": "jest",
18 | "semantic-release": "semantic-release"
19 | },
20 | "peerDependencies": {
21 | "react": "^16.13.1",
22 | "react-dom": "^16.13.1"
23 | },
24 | "bin": {
25 | "flareact": "./src/bin/flareact.js"
26 | },
27 | "dependencies": {
28 | "@babel/core": "^7.11.0",
29 | "@babel/plugin-transform-runtime": "^7.11.0",
30 | "@babel/preset-env": "^7.11.0",
31 | "@babel/preset-react": "^7.10.4",
32 | "@babel/runtime": "^7.11.0",
33 | "@cloudflare/kv-asset-handler": "^0.0.11",
34 | "@pmmmwh/react-refresh-webpack-plugin": "^0.4.1",
35 | "babel-loader": "^8.1.0",
36 | "babel-plugin-react-require": "^3.1.3",
37 | "concurrently": "^5.2.0",
38 | "copy-webpack-plugin": "^5",
39 | "css-loader": "^4.2.1",
40 | "dotenv": "^8.2.0",
41 | "glob": "^7.1.6",
42 | "mini-css-extract-plugin": "^0.9.0",
43 | "mitt": "^2.1.0",
44 | "nanoid": "^3.1.12",
45 | "null-loader": "^4.0.1",
46 | "optimize-css-assets-webpack-plugin": "^5.0.3",
47 | "postcss-flexbugs-fixes": "^4.2.1",
48 | "postcss-loader": "^3.0.0",
49 | "postcss-preset-env": "^6.7.0",
50 | "react-helmet": "^6.1.0",
51 | "react-refresh": "^0.8.3",
52 | "style-loader": "^1.2.1",
53 | "terser-webpack-plugin": "^4.0.0",
54 | "webpack": "^4.44.1",
55 | "webpack-cli": "^3.3.12",
56 | "webpack-dev-server": "^3.11.0",
57 | "webpack-merge": "^5.1.1",
58 | "webpack-sources": "^1.4.3",
59 | "yargs": "^16.1.0"
60 | },
61 | "devDependencies": {
62 | "@babel/plugin-proposal-optional-chaining": "^7.11.0",
63 | "@testing-library/react": "^11.1.0",
64 | "babel-jest": "^26.2.2",
65 | "jest": "^26.2.2",
66 | "react": "^16.13.1",
67 | "react-dom": "^16.13.1",
68 | "semantic-release": "^17.1.1"
69 | },
70 | "repository": {
71 | "type": "git",
72 | "url": "https://github.com/flareact/flareact.git"
73 | },
74 | "release": {
75 | "branches": [
76 | "+([0-9])?(.{+([0-9]),x}).x",
77 | "main",
78 | {
79 | "name": "canary",
80 | "prerelease": true
81 | },
82 | {
83 | "name": "alpha",
84 | "prerelease": true
85 | },
86 | {
87 | "name": "beta",
88 | "prerelease": true
89 | }
90 | ]
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/docs/deployment.md:
--------------------------------------------------------------------------------
1 | # Deployment
2 |
3 | To deploy your Flareact site to Cloudflare, run:
4 |
5 | ```bash
6 | yarn deploy
7 | ```
8 |
9 | Behind the scenes, Flareact bundles your client assets in a production-ready format and then runs `wrangler publish`.
10 |
11 | ## Deploying to a custom route
12 |
13 | In your `wrangler.toml` file, you can use specify a route for your site instead of the `workers_dev` default host.
14 |
15 | To set a route, change `workers_dev = false` and specify your route:
16 |
17 | ```toml
18 | workers_dev = false
19 | route = "flareact.com/*"
20 | ```
21 |
22 | At this point, you'll also be required to include a `zone_id`. If you don't want to include this in your version control (e.g. your project is open-source), you can define it in a local `.env` file which is ignored by Git:
23 |
24 | ```
25 | CF_ZONE_ID=
26 | CF_ACCOUNT_ID=
27 | ```
28 |
29 | **Note:**: Per the [Cloudflare Docs](https://developers.cloudflare.com/workers/learning/getting-started#6d-configuring-your-project), if your route is configured to a hostname, you will need to add a DNS record to Cloudflare to ensure that the hostname can be resolved externally. If your Worker acts as your origin (the response comes directly from a Worker), you should enter a placeholder (dummy) AAAA record pointing to `100::`, which is the [reserved IPv6 discard prefixOpen external link](https://tools.ietf.org/html/rfc6666).
30 |
31 | ## Deploying to environments
32 |
33 | Cloudflare allows you to [define additional environments](https://developers.cloudflare.com/workers/platform/environments) for your Workers site.
34 |
35 | After adding a new environment to your `wrangler.toml` file:
36 |
37 | ```toml
38 | [env.staging]
39 | name = "your-site-staging"
40 | workers_dev = true
41 | ```
42 |
43 | You can pass the `--env ` flag to the `yarn deploy` command:
44 |
45 | ```bash
46 | yarn deploy --env staging
47 | ```
48 |
49 | ## Deploying from GitHub Actions
50 |
51 | To deploy from GitHub Actions, you can use the [wrangler action](https://github.com/cloudflare/wrangler-action).
52 |
53 | In your project, create a `.github/workflows/deploy.yml` file:
54 |
55 | ```yaml
56 | name: Deploy
57 | on:
58 | - push
59 | jobs:
60 | deploy:
61 | runs-on: ubuntu-latest
62 | timeout-minutes: 10
63 | steps:
64 | - uses: actions/checkout@v2
65 | - uses: actions/setup-node@v1
66 | with:
67 | node-version: 12
68 | - run: yarn install
69 | - run: yarn build
70 | - name: Publish
71 | uses: cloudflare/wrangler-action@1.2.0
72 | with:
73 | apiToken: ${{ secrets.CF_API_TOKEN }}
74 | env:
75 | CF_ACCOUNT_ID: ${{ secrets.CF_ACCOUNT_ID }}
76 | IS_WORKER: true
77 | ```
78 |
79 | Optionally update your `push:` directive to include only certain branches.
80 |
81 | You also might want to add a `CF_ZONE_ID` if you're deploying to a custom domain.
82 |
--------------------------------------------------------------------------------
/src/worker/pages.js:
--------------------------------------------------------------------------------
1 | import App from "../components/_app";
2 |
3 | export const DYNAMIC_PAGE = new RegExp("\\[(\\w+)\\]", "g");
4 |
5 | export function resolvePagePath(pagePath, keys) {
6 | const pagesMap = keys.map((page) => {
7 | let test = page;
8 | let parts = [];
9 |
10 | const isDynamic = DYNAMIC_PAGE.test(page);
11 |
12 | if (isDynamic) {
13 | for (const match of page.matchAll(/\[(\w+)\]/g)) {
14 | parts.push(match[1]);
15 | }
16 |
17 | test = test.replace(DYNAMIC_PAGE, () => "([\\w_-]+)");
18 | }
19 |
20 | test = test.replace("/", "\\/").replace(/^\./, "").replace(/\.js$/, "");
21 |
22 | return {
23 | page,
24 | pagePath: page.replace(/^\./, "").replace(/\.js$/, ""),
25 | parts,
26 | test: new RegExp("^" + test + "$", isDynamic ? "g" : ""),
27 | };
28 | });
29 |
30 | /**
31 | * Sort pages to include those with `index` in the name first, because
32 | * we need those to get matched more greedily than their dynamic counterparts.
33 | */
34 | pagesMap.sort((a) => (a.page.includes("index") ? -1 : 1));
35 |
36 | let page = pagesMap.find((p) => p.test.test(pagePath));
37 |
38 | /**
39 | * If an exact match couldn't be found, try giving it another shot with /index at
40 | * the end. This helps discover dynamic nested index pages.
41 | */
42 | if (!page) {
43 | page = pagesMap.find((p) => p.test.test(pagePath + "/index"));
44 | }
45 |
46 | if (!page) return null;
47 | if (!page.parts.length) return page;
48 |
49 | let params = {};
50 |
51 | page.test.lastIndex = 0;
52 |
53 | const matches = pagePath.matchAll(page.test);
54 |
55 | for (const match of matches) {
56 | page.parts.forEach((part, idx) => (params[part] = match[idx + 1]));
57 | }
58 |
59 | page.params = params;
60 |
61 | return page;
62 | }
63 |
64 | export function getPage(pagePath, context) {
65 | try {
66 | const resolvedPage = resolvePagePath(pagePath, context.keys());
67 | const page = context(resolvedPage.page);
68 |
69 | return {
70 | ...resolvedPage,
71 | ...page,
72 | };
73 | } catch (e) {
74 | if (pagePath === "/_app") {
75 | return { default: App };
76 | }
77 |
78 | throw new PageNotFoundError();
79 | }
80 | }
81 |
82 | export async function getPageProps(page, query) {
83 | let pageProps = {};
84 |
85 | const params = page.params || {};
86 |
87 | const fetcher = page.getEdgeProps || page.getStaticProps;
88 |
89 | const queryObject = {
90 | ...query,
91 | ...params,
92 | };
93 |
94 | if (fetcher) {
95 | const { props, revalidate } = await fetcher({ params, query: queryObject });
96 |
97 | pageProps = {
98 | ...props,
99 | revalidate,
100 | };
101 | }
102 |
103 | return pageProps;
104 | }
105 |
106 | export class PageNotFoundError extends Error {}
107 |
--------------------------------------------------------------------------------
/docs/index.md:
--------------------------------------------------------------------------------
1 | # Flareact
2 |
3 | Flareact is an **edge-rendered** React framework built for [Cloudflare Workers](https://workers.cloudflare.com/).
4 |
5 | It features **file-based page routing** with dynamic page paths and **edge-side data fetching** APIs.
6 |
7 | Flareact is modeled after the terrific [Next.js](https://nextjs.org/) project and its APIs. If you're transitioning from Next.js, a lot of the APIs will seem familiar, _if not identical_!
8 |
9 | [](https://deploy.workers.cloudflare.com/?url=https://github.com/flareact/flareact-template&paid=true)
10 |
11 | ## Overview
12 |
13 | Flareact will serve each file in the `/pages` directory under a pathname matching the filename.
14 |
15 | For example, the following component at `/pages/about.js`:
16 |
17 | ```js
18 | export default function About() {
19 | return
Who we are
;
20 | }
21 | ```
22 |
23 | The above page will be served at `site.com/about`.
24 |
25 | Next step: Read the [getting started guide](/docs/getting-started).
26 |
27 | ## But why?
28 |
29 | Right — *another* React framework. Here's why Flareact might be useful to you:
30 |
31 | - **Server-Side Rendering** (technically _edge-side_ rendering): Return the HTML output of your site in the initial request, rather than waiting for the client to render it. This can be helpful for SEO and initial time-to-first-paint/time-to-interactive.
32 | - **Cloudflare**: If you already point your site's DNS to Cloudflare, you might as well host your site there, too! No need to find additional hosting, wire up DNS records, or deal with SSL provisioning issues between services.
33 | - **Speed**: Because your site is being generated and served directly from the Cloudflare edge network, you're reducing network hops between the CDN and your content host. This means your site is delivered _within milliseconds_. Cloudflare even [eliminates cold starts using the TLS handshake period](https://blog.cloudflare.com/eliminating-cold-starts-with-cloudflare-workers/).
34 | - **Infinitely scalable**: Don't worry about scaling up servers or getting hit with steep service provider fees. Cloudflare's network is built to handle huge amounts of traffic with a predictable pricing model. Plus, by caching your page data at the edge with an optional invalidation strategy, you can host dynamic content as if it were a statically-generated site.
35 | - **Familiar API**: Next.js did the hard work to create a great developer experience (DX) for creating and maintaining modern React applications. Flareact borrows many patterns from Next.js, so you'll feel right at home developing your Flareact site.
36 |
37 | ## Examples
38 |
39 | - [Flareact Docs (this site)](https://github.com/flareact/flareact-site/)
40 | - [Headless CMS: WordPress](https://github.com/flareact/flareact/tree/master/examples/with-cms-wordpress)
41 |
42 | ## About
43 |
44 | Flareact is an experiment created by [Josh Larson](https://www.jplhomer.org/) in August 2020.
45 |
46 | Lots of inspiration and thanks to:
47 |
48 | - [Next.js](https://nextjs.org) (obviously)
49 | - [SWR](https://swr.vercel.app/) for this site's design inspiration
50 | - [Tailwind](https://tailwindcss.com) for the styles
51 | - [Kari Linder](https://twitter.com/kkblinder) from Cloudflare for the logo 🔥
52 |
--------------------------------------------------------------------------------
/src/bin/flareact.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | const concurrently = require("concurrently");
3 | const dotenv = require("dotenv");
4 | dotenv.config();
5 |
6 | ["react", "react-dom"].forEach((dependency) => {
7 | try {
8 | require.resolve(dependency);
9 | } catch (err) {
10 | console.warn(
11 | `The module '${dependency}' was not found. Flareact requires that you include it in 'dependencies' of your 'package.json'. To add it, run 'npm install ${dependency}'`
12 | );
13 | }
14 | });
15 |
16 | const yargs = require("yargs");
17 |
18 | const argv = yargs
19 | .command("dev", "Starts a Flareact development server")
20 | .command("publish", "Builds Flareact for production and deploys it", {
21 | env: {
22 | description: "The Cloudflare Workers environment to target",
23 | type: "string",
24 | },
25 | })
26 | .command("build", "Builds Flareact for production")
27 | .help()
28 | .command({
29 | command: "*",
30 | handler() {
31 | yargs.showHelp();
32 | },
33 | })
34 | .demandCommand()
35 | .alias("help", "h").argv;
36 |
37 | if (argv._.includes("dev")) {
38 | console.log("Starting Flareact dev server...");
39 |
40 | concurrently(
41 | [
42 | {
43 | command: "wrangler dev",
44 | name: "worker",
45 | env: { WORKER_DEV: true, IS_WORKER: true },
46 | },
47 | {
48 | command:
49 | "webpack-dev-server --config node_modules/flareact/configs/webpack.client.config.js --mode development",
50 | name: "client",
51 | env: { NODE_ENV: "development" },
52 | },
53 | ],
54 | {
55 | prefix: "name",
56 | killOthers: ["failure"],
57 | restartTries: 0,
58 | }
59 | ).then(
60 | () => {},
61 | (error) => {
62 | console.error(error);
63 | }
64 | );
65 | }
66 |
67 | if (argv._.includes("publish")) {
68 | const destination = argv.env ? `${argv.env} on Cloudflare` : "Cloudflare";
69 |
70 | console.log(`Publishing your Flareact project to ${destination}...`);
71 |
72 | let wranglerPublish = `wrangler publish`;
73 |
74 | if (argv.env) {
75 | wranglerPublish += ` --env ${argv.env}`;
76 | }
77 |
78 | concurrently(
79 | [
80 | {
81 | command: `webpack --config node_modules/flareact/configs/webpack.client.config.js --out ./out --mode production && ${wranglerPublish}`,
82 | name: "publish",
83 | env: { NODE_ENV: "production", IS_WORKER: true },
84 | },
85 | ],
86 | {
87 | prefix: "name",
88 | killOthers: ["failure"],
89 | restartTries: 0,
90 | }
91 | ).then(
92 | () => {},
93 | (error) => {
94 | console.error(error);
95 | }
96 | );
97 | }
98 |
99 | if (argv._.includes("build")) {
100 | console.log("Building your Flareact project for production...");
101 |
102 | concurrently(
103 | [
104 | {
105 | command:
106 | "webpack --config node_modules/flareact/configs/webpack.client.config.js --out ./out --mode production",
107 | name: "publish",
108 | env: { NODE_ENV: "production" },
109 | },
110 | ],
111 | {
112 | prefix: "name",
113 | killOthers: ["failure"],
114 | restartTries: 0,
115 | }
116 | ).then(
117 | () => {},
118 | (error) => {
119 | console.error(error);
120 | }
121 | );
122 | }
123 |
--------------------------------------------------------------------------------
/src/components/_document.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { htmlEscapeJsonString } from "../utils";
3 |
4 | const dev = typeof DEV !== "undefined" && !!DEV;
5 |
6 | export default function Document({
7 | initialData,
8 | helmet,
9 | page,
10 | context,
11 | buildManifest,
12 | }) {
13 | const htmlAttrs = helmet.htmlAttributes.toComponent();
14 | const bodyAttrs = helmet.bodyAttributes.toComponent();
15 | let currentPage = page.page.replace(/^\./, "").replace(/\.(js|css)$/, "");
16 |
17 | // Flatten dynamic `index.js` pages
18 | if (currentPage !== "/index" && currentPage.endsWith("/index")) {
19 | currentPage = currentPage.replace(/\/index$/, "");
20 | }
21 |
22 | // TODO: Drop all these props into a context and consume them in individual components
23 | // so this page can be extended.
24 |
25 | return (
26 |
27 |
32 |
33 |
34 |
40 |
41 |
42 | );
43 | }
44 |
45 | export function FlareactHead({ helmet, page, buildManifest }) {
46 | let links = new Set();
47 |
48 | if (!dev) {
49 | buildManifest.pages["/_app"]
50 | .filter((link) => link.endsWith(".css"))
51 | .forEach((link) => links.add(link));
52 | buildManifest.pages[page]
53 | .filter((link) => link.endsWith(".css"))
54 | .forEach((link) => links.add(link));
55 | }
56 |
57 | return (
58 |
59 |
60 |
61 | {helmet.title.toComponent()}
62 | {helmet.meta.toComponent()}
63 | {helmet.link.toComponent()}
64 |
65 | {[...links].map((link) => (
66 |
67 | ))}
68 |
69 | );
70 | }
71 |
72 | export function FlareactScripts({ initialData, page, buildManifest }) {
73 | let prefix = dev ? "http://localhost:8080/" : "/";
74 | prefix += dev ? "" : "_flareact/static/";
75 |
76 | let scripts = new Set();
77 |
78 | if (dev) {
79 | [
80 | "webpack.js",
81 | "main.js",
82 | `pages/_app.js`,
83 | `pages${page}.js`,
84 | ].forEach((script) => scripts.add(script));
85 | } else {
86 | buildManifest.helpers.forEach((script) => scripts.add(script));
87 | buildManifest.pages["/_app"]
88 | .filter((script) => script.endsWith(".js"))
89 | .forEach((script) => scripts.add(script));
90 | buildManifest.pages[page]
91 | .filter((script) => script.endsWith(".js"))
92 | .forEach((script) => scripts.add(script));
93 | }
94 |
95 | return (
96 | <>
97 |
104 | {[...scripts].map((script) => (
105 |
106 | ))}
107 | >
108 | );
109 | }
110 |
--------------------------------------------------------------------------------
/docs/getting-started.md:
--------------------------------------------------------------------------------
1 | # Getting Started
2 |
3 | Howdy! Let's get you set up with Flareact.
4 |
5 | It's important to know that you must have a [Cloudflare account](https://cloudflare.com/) with a **paid (\$5/mo) Bundled plan** to use Flareact.
6 |
7 | ## Quickstart
8 |
9 | If you want to get started right now, you can click the button below to fork the `flareact-template` repo and set up your account without installing any CLI tools:
10 |
11 | [](https://deploy.workers.cloudflare.com/?url=https://github.com/flareact/flareact-template&paid=true)
12 |
13 | Otherwise, follow the instructions below to get started.
14 |
15 | ## Installation
16 |
17 | Make sure you have the [wrangler CLI](https://github.com/cloudflare/wrangler) tool installed:
18 |
19 | ```bash
20 | npm i @cloudflare/wrangler -g
21 | ```
22 |
23 | You may need to run `wrangler login` to authenticate your Cloudflare account.
24 |
25 | Next, use wrangler to create a new Flareact project using the [template](https://github.com/flareact/flareact-template):
26 |
27 | ```bash
28 | wrangler generate my-project https://github.com/flareact/flareact-template
29 | ```
30 |
31 | Finally, switch to your directory and yun `yarn` or `npm install`:
32 |
33 | ```bash
34 | cd my-project
35 | yarn
36 | ```
37 |
38 | ## Manual Installation
39 |
40 | To add Flareact to an existing project, add `flareact` as a dependency:
41 |
42 | ```js
43 | yarn add flareact
44 | ```
45 |
46 | Or using npm:
47 |
48 | ```js
49 | npm install flareact
50 | ```
51 |
52 | Next, make sure you have the following files (check out the [template repo](https://github.com/flareact/flareact-template) to see the contents of each):
53 |
54 | - `index.js`
55 | - `wrangler.toml`
56 |
57 | Open `package.json` file and add the following `scripts`:
58 |
59 | ```json
60 | "scripts": {
61 | "dev": "flareact dev",
62 | "build": "flareact build",
63 | "deploy": "flareact publish"
64 | }
65 | ```
66 |
67 | These scripts refer to the different stages of developing an application:
68 |
69 | - `dev` - Runs `flareact dev` which kicks off `wrangler dev` and `flareact` in development mode
70 | - `build` - Runs `flareact build` which kicks creates a production client-side bundle (useful for deploying from CI)
71 | - `deploy` - Runs `flareact publish` which builds your application and runs `wrangler publish` to deploy it
72 |
73 | Flareact uses the concept of pages. A page is a React Component exported from a `.js` file in the `pages` directory.
74 |
75 | Pages are associated with a route based on their file name. For example, `pages/about.js` is mapped to `site.com/about`. You can even add dynamic route parameters with the filename.
76 |
77 | You will need **at least one** page inside your `/pages` directory. To add a landing/index page, add the following to `pages/index.js`:
78 |
79 | ```js
80 | export default function Index() {
81 | return
Home
;
82 | }
83 | ```
84 |
85 | ## Development
86 |
87 | To preview your Flareact site locally, run `yarn dev` in your terminal. Behind the scenes, Wrangler creates a tunnel from your local site to Cloudflare's edge — bringing your development and production environments closer together.
88 |
89 | By default, your site will be available at [http://127.0.0.1:8787/](http://127.0.0.1:8787/).
90 |
91 | **Note**: Be sure to fill in your `account_id` in `wrangler.toml`. You can also add it to a local `.env` file in your project:
92 |
93 | ```bash
94 | CF_ACCOUNT_ID=youraccountid
95 | ```
96 |
97 | Or pass it to `yarn dev` as an environment variable:
98 |
99 | ```bash
100 | CF_ACCOUNT_ID=youraccountid yarn dev
101 | ```
102 |
--------------------------------------------------------------------------------
/docs/data-fetching.md:
--------------------------------------------------------------------------------
1 | # Data Fetching
2 |
3 | Flareact allows you to fetch data for page components. This happens in in your Cloudflare Worker on the edge, using `getEdgeProps`.
4 |
5 | By default, **all edge props are cached** for the lifetime of your current deployment revision, but you can change that behavior using the `revalidate` property.
6 |
7 | ## Fetching Data using `getEdgeProps`
8 |
9 | To define props for your component, export an asynchronous `getEdgeProps` function from your React component:
10 |
11 | ```js
12 | export async function getEdgeProps() {
13 | const posts = await getBlogPosts();
14 |
15 | return {
16 | props: {
17 | posts,
18 | },
19 | };
20 | }
21 |
22 | export default function Posts({ posts }) {
23 | return (
24 |