diff --git a/README.md b/README.md index 6f53e9b..b2bb0a7 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,15 @@ -# Serverless Microapp +# Serverless React App +Domain: `cf.neweradesign.net` ## Client Hosted on Cloudflare Workers +- uses Wrangler for orchestration - uses itty-router for Express-like routing -- uses handlebars for views engine +- uses React for client side framework +- uses custom webpack config for build - 100,000 free requests per day ## Server Hosted on AWS Lambda +- uses AWS CDK for orchestration - uses node engine - best for database operations (i.e. working with MongoDB) - 10,000 free requests per day diff --git a/client/package.json b/client/package.json index 1acdd43..a527f1f 100644 --- a/client/package.json +++ b/client/package.json @@ -5,25 +5,30 @@ "description": "tech tools hosted on cloudflare worker", "main": "index.js", "scripts": { - "build": "npm run compilehbs && npm run transpilehbs && rm src/*-original.js", - "compilehbs": "handlebars -e hbs -f src/pages-original.js src/views/pages/ && handlebars -e hbs -p -f src/partials-original.js src/views/partials/", + "build": "webpack", "deploy": "wrangler deploy src/index.js", "prepare": "husky", - "start": "wrangler dev src/index.js", - "test": "echo \"Error: no test specified\" && exit 1", - "transpilehbs": "hbs-import-transpile src/pages-original.js > assets/pages.js && hbs-import-transpile src/partials-original.js > assets/partials.js" + "start": "wrangler dev src/index.js" }, - "type": "module", "author": "", "license": "ISC", "devDependencies": { - "hbs-import-transpile": "^1.0.4" + "@babel/core": "^7.24.8", + "@babel/preset-env": "^7.24.8", + "@babel/preset-react": "^7.24.7", + "babel-loader": "^9.1.3", + "css-loader": "^7.1.2", + "file-loader": "^6.2.0", + "style-loader": "^4.0.0", + "webpack": "^5.93.0", + "webpack-cli": "^5.1.4" }, "dependencies": { + "@cloudflare/kv-asset-handler": "^0.3.4", "buffer": "^6.0.3", - "handlebars": "^4.7.8", - "hbs-async-render": "^1.0.1", "itty-router": "^2.6.6", + "react": "^18.3.1", + "react-dom": "^18.3.1", "serverless-cloudflare-workers": "^1.2.0" } } diff --git a/client/src/views/partials/logos/cloudflare.hbs b/client/public/cloudflare.svg similarity index 94% rename from client/src/views/partials/logos/cloudflare.hbs rename to client/public/cloudflare.svg index 4df92a9..f0b1152 100644 --- a/client/src/views/partials/logos/cloudflare.hbs +++ b/client/public/cloudflare.svg @@ -3,19 +3,19 @@ diff --git a/client/src/App.jsx b/client/src/App.jsx new file mode 100644 index 0000000..969bc67 --- /dev/null +++ b/client/src/App.jsx @@ -0,0 +1,34 @@ +import React, { useState } from 'react'; +import logo from '../public/cloudflare.svg' +import { Header } from "./components/Header/Header"; +import { Footer } from "./components/Footer/Footer"; + +export const App = () => { + const [count, setCount] = useState(0); + const tester = "Victor E"; + + return ( +
+
+
+
+
+
+ +
+
+

Hello, Cloudflare Workers!

+

This is a basic React page deployed on Cloudflare Workers.

+

+ Your name: { tester } +

+
{ tester }
+ +

Count: { count }

+ +
+
+
+ ); +}; \ No newline at end of file diff --git a/client/src/components/Footer/Footer.jsx b/client/src/components/Footer/Footer.jsx new file mode 100644 index 0000000..e4bdff5 --- /dev/null +++ b/client/src/components/Footer/Footer.jsx @@ -0,0 +1,5 @@ +export const Footer = () => ( + +); \ No newline at end of file diff --git a/client/src/components/Header/Header.jsx b/client/src/components/Header/Header.jsx new file mode 100644 index 0000000..07d334e --- /dev/null +++ b/client/src/components/Header/Header.jsx @@ -0,0 +1,7 @@ +export const Header = () => ( + +); \ No newline at end of file diff --git a/client/src/index.js b/client/src/index.js index fda9fb6..a2d02fa 100644 --- a/client/src/index.js +++ b/client/src/index.js @@ -1,29 +1,34 @@ -import { Router } from 'itty-router' -import { base64Handler, postHandler, rootHandler } from './routers' -import { registerHBHelper } from './utils/hbsAsyncHelper.js' -import '../assets/pages.js'; -import '../assets/partials.js'; +import { Router } from 'itty-router'; +import * as routes from "./routers"; -// Register HB -registerHBHelper(); - -// Create a new router const router = Router(); -router.get("/", rootHandler); + +const { base64Handler, healthHandler, postHandler, rootHandler, routesAndAssetsHandler } = routes; + +/** The rootHandler will serve the React app accordingly + * Other routes can be defined as neeeded + */ +router.get('/', rootHandler); +// Health route +router.get('/health', healthHandler); +// Test route router.get("/base64/:text", base64Handler); +// Test post route router.post("/post", postHandler); /** * This is the last route we define, it will match anything that hasn't hit a route we've defined * above, therefore it's useful as a 404 (and avoids us hitting worker exceptions, so make sure * to include it!). -*/ + */ router.all("*", () => new Response("404, not found!", { status: 404 })); /** - * This snippet ties our worker to the router we defined above, all incoming requests - * are passed to the router where your routes are called and the response is sent. -*/ -addEventListener('fetch', (e) => { - e.respondWith(router.handle(e.request)) + * All incoming requests to the worker are passed to the router + * where your routes are called and the response is sent. + * routesAndAssetsHandler will map assets and routes accordingly + */ +addEventListener('fetch', event => { + event.respondWith(routesAndAssetsHandler(event, router)); }); + diff --git a/client/src/main.js b/client/src/main.js new file mode 100644 index 0000000..25392e3 --- /dev/null +++ b/client/src/main.js @@ -0,0 +1,6 @@ +import { App } from './App'; +import { createRoot } from 'react-dom/client'; + +// Render your React component instead +const root = createRoot(document.getElementById('root')); +root.render(); diff --git a/client/src/routers/handler.js b/client/src/routers/handler.js new file mode 100644 index 0000000..b510595 --- /dev/null +++ b/client/src/routers/handler.js @@ -0,0 +1,27 @@ +import { getAssetFromKV } from "@cloudflare/kv-asset-handler"; + +export const routesAndAssetsHandler = async (event, router) => { + // Extract the request from the event + const request = event.request; + + // Check if the request is for bundle.js + if ( + request.url.endsWith('/bundle.js') + || request.url.endsWith('/favicon.ico') + || request.url.endsWith('.svg') + ) { + // Serve the bundle.js file from KV storage + try { + // Pass the entire event object to getAssetFromKV + return await getAssetFromKV(event); + } catch (e) { + return new Response(`Bundle not found: ${e.message}`, { + status: 404, + statusText: 'Not Found', + }); + } + } + + // For any other requests, handle them with the router + return router.handle(request); +}; \ No newline at end of file diff --git a/client/src/routers/health/router.js b/client/src/routers/health/router.js new file mode 100644 index 0000000..8874958 --- /dev/null +++ b/client/src/routers/health/router.js @@ -0,0 +1 @@ +export const healthHandler = () => new Response("success"); \ No newline at end of file diff --git a/client/src/routers/index.js b/client/src/routers/index.js index 03606a1..0e64c0b 100644 --- a/client/src/routers/index.js +++ b/client/src/routers/index.js @@ -1,9 +1,13 @@ import { base64Handler } from './encoding/base64/router.js'; +import { healthHandler } from './health/router' import { postHandler } from './post/router.js'; import { rootHandler } from './root/router.js'; +import { routesAndAssetsHandler } from './handler'; export { base64Handler, + healthHandler, postHandler, - rootHandler + rootHandler, + routesAndAssetsHandler } \ No newline at end of file diff --git a/client/src/routers/root/html.js b/client/src/routers/root/html.js deleted file mode 100644 index 385e715..0000000 --- a/client/src/routers/root/html.js +++ /dev/null @@ -1,6 +0,0 @@ -export const html = ` - - -

Hello, world! This is the root page of your Worker template.

- `; \ No newline at end of file diff --git a/client/src/routers/root/router.js b/client/src/routers/root/router.js index 5d6d40c..8bf444e 100644 --- a/client/src/routers/root/router.js +++ b/client/src/routers/root/router.js @@ -1,14 +1,25 @@ -import Handlebars from 'handlebars/runtime.js'; - -import { hbsAsyncRender } from 'hbs-async-render' -// import { html } from './html'; - export const rootHandler = async () => { - const output = await hbsAsyncRender(Handlebars, 'body', {name: "Victor E."}); - return new Response(output, {headers: {'Content-Type': 'text/html'}}); - // return new Response(html, { - // headers: { - // "Content-Type": "text/html" - // } - // }) + // const html = ReactDOMServer.renderToString(); + return new Response(` + + + + + + React App on Cloudflare Workers + + + + + + +
+ + + + `, { + headers: { + 'Content-Type': 'text/html', + }, + }); }; \ No newline at end of file diff --git a/client/src/utils/hbsAsyncHelper.js b/client/src/utils/hbsAsyncHelper.js deleted file mode 100644 index 8b2d577..0000000 --- a/client/src/utils/hbsAsyncHelper.js +++ /dev/null @@ -1,20 +0,0 @@ -import Handlebars from 'handlebars/runtime.js'; - -import { registerAsyncHelper } from 'hbs-async-render' - -/** - * Register an asynchronous helper that waits for a second and then resolves with some information - * that is going to be rendered in the place where `asyncTest` has been used in the Handlebar templates. - */ -export const registerHBHelper = () => registerAsyncHelper(Handlebars,'asyncTest', function (options, context) { - - return new Promise((resolve, reject) => { - setTimeout( - function() { - resolve(`Async render with params: ${options.hash.name}`) - }, - 200 - ); - }); - -}) diff --git a/client/src/views/pages/body.hbs b/client/src/views/pages/body.hbs deleted file mode 100644 index 7b80430..0000000 --- a/client/src/views/pages/body.hbs +++ /dev/null @@ -1,35 +0,0 @@ - - - {{> structure/htmlhead }} - - Cloudflare Workers with Handlebars - - - -{{> structure/header }} - -
-
-
- -
-
- {{> logos/cloudflare }} -
-
- -

This template is rendered by Handlebars in a Cloudflare Worker!

- -

- Your name: {{name}} -

- -
{{asyncTest name='Vic Tester'}}
- -
-
-
- -{{> structure/footer }} - - \ No newline at end of file diff --git a/client/src/views/partials/logos/handlebars.hbs b/client/src/views/partials/logos/handlebars.hbs deleted file mode 100644 index e69de29..0000000 diff --git a/client/src/views/partials/structure/footer.hbs b/client/src/views/partials/structure/footer.hbs deleted file mode 100644 index a5ccfb3..0000000 --- a/client/src/views/partials/structure/footer.hbs +++ /dev/null @@ -1,3 +0,0 @@ -
- Footer component. -
\ No newline at end of file diff --git a/client/src/views/partials/structure/footerlibs.hbs b/client/src/views/partials/structure/footerlibs.hbs deleted file mode 100644 index e4fd0f6..0000000 --- a/client/src/views/partials/structure/footerlibs.hbs +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/client/src/views/partials/structure/header.hbs b/client/src/views/partials/structure/header.hbs deleted file mode 100644 index 1fed4d2..0000000 --- a/client/src/views/partials/structure/header.hbs +++ /dev/null @@ -1,6 +0,0 @@ - -
\ No newline at end of file diff --git a/client/src/views/partials/structure/htmlhead.hbs b/client/src/views/partials/structure/htmlhead.hbs deleted file mode 100644 index 7cbddd4..0000000 --- a/client/src/views/partials/structure/htmlhead.hbs +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/client/webpack.config.js b/client/webpack.config.js new file mode 100644 index 0000000..2b717a7 --- /dev/null +++ b/client/webpack.config.js @@ -0,0 +1,41 @@ +const path = require('path'); + +module.exports = { + mode: 'production', + entry: './src/main.js', + output: { + filename: 'bundle.js', + path: path.resolve(__dirname, 'dist'), + }, + module: { + rules: [ + { + test: /\.(js|jsx)$/, + exclude: /node_modules/, + use: { + loader: 'babel-loader', + options: { + presets: ['@babel/preset-env', '@babel/preset-react'], + }, + }, + }, + { + test: /\.css$/, + use: ['style-loader', 'css-loader'], + }, + { + test: /\.(png|jpg|svg)$/, + use: { + loader: 'file-loader', + options: { + name: '[name].[hash].[ext]', + outputPath: 'images/', + }, + }, + }, + ], + }, + resolve: { + extensions: ['.js', '.jsx'], + }, +}; diff --git a/client/wrangler.toml b/client/wrangler.toml index 52d77b7..f0d3b68 100644 --- a/client/wrangler.toml +++ b/client/wrangler.toml @@ -2,11 +2,14 @@ name = "tech" account_id = "**********" workers_dev = true compatibility_date = "2024-07-11" +main = "index.js" + +[site] +bucket = "./dist" + +[build] +command = "npm run build" [route] pattern = "**********" custom_domain = true - -[build] -command="npm run build" -watch_dir="views/" \ No newline at end of file