From d45c4b164f8f93e481aa71b1e2cbbe9edbfaa07b Mon Sep 17 00:00:00 2001 From: Jonas Galvez Date: Sun, 19 Jun 2022 17:00:05 -0300 Subject: [PATCH 01/14] rm old package --- packages/fastify-dx-vue/index.js | 0 packages/fastify-dx-vue/package.json | 6 ------ 2 files changed, 6 deletions(-) delete mode 100644 packages/fastify-dx-vue/index.js delete mode 100644 packages/fastify-dx-vue/package.json diff --git a/packages/fastify-dx-vue/index.js b/packages/fastify-dx-vue/index.js deleted file mode 100644 index e69de29..0000000 diff --git a/packages/fastify-dx-vue/package.json b/packages/fastify-dx-vue/package.json deleted file mode 100644 index 586576e..0000000 --- a/packages/fastify-dx-vue/package.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "fastify-dx-vue", - "version": "0.0.0", - "files": ["index.js"], - "license": "MIT" -} \ No newline at end of file From a40b2f2e1eaa016f64e3c727bcab2e445b407e92 Mon Sep 17 00:00:00 2001 From: Jonas Galvez Date: Sun, 19 Jun 2022 17:03:36 -0300 Subject: [PATCH 02/14] start from react --- packages/fastify-dx-vue/.eslintrc | 32 ++++ packages/fastify-dx-vue/README.md | 90 +++++++++ packages/fastify-dx-vue/index.js | 177 ++++++++++++++++++ packages/fastify-dx-vue/package.json | 48 +++++ packages/fastify-dx-vue/plugin.cjs | 107 +++++++++++ packages/fastify-dx-vue/server/context.js | 65 +++++++ packages/fastify-dx-vue/server/stream.js | 53 ++++++ packages/fastify-dx-vue/virtual/context.js | 4 + packages/fastify-dx-vue/virtual/core.jsx | 140 ++++++++++++++ packages/fastify-dx-vue/virtual/create.jsx | 7 + packages/fastify-dx-vue/virtual/layouts.js | 14 ++ .../virtual/layouts/default.jsx | 12 ++ packages/fastify-dx-vue/virtual/mount.js | 47 +++++ packages/fastify-dx-vue/virtual/resource.js | 68 +++++++ packages/fastify-dx-vue/virtual/root.jsx | 10 + packages/fastify-dx-vue/virtual/routes.js | 121 ++++++++++++ starters/vue/.eslintignore | 1 + starters/vue/.eslintrc | 31 +++ starters/vue/client/assets/logo.svg | 84 +++++++++ starters/vue/client/base.css | 56 ++++++ starters/vue/client/context.js | 36 ++++ starters/vue/client/index.html | 13 ++ starters/vue/client/index.js | 8 + starters/vue/client/layouts/auth.jsx | 25 +++ starters/vue/client/layouts/default.jsx | 9 + starters/vue/client/pages/client-only.jsx | 23 +++ starters/vue/client/pages/index.jsx | 35 ++++ starters/vue/client/pages/server-only.jsx | 17 ++ starters/vue/client/pages/streaming.jsx | 46 +++++ starters/vue/client/pages/using-auth.jsx | 39 ++++ starters/vue/client/pages/using-data.jsx | 46 +++++ starters/vue/client/pages/using-store.jsx | 36 ++++ starters/vue/client/root.jsx | 11 ++ starters/vue/package.json | 45 +++++ starters/vue/postcss.config.cjs | 9 + starters/vue/server.js | 32 ++++ starters/vue/vite.config.js | 29 +++ 37 files changed, 1626 insertions(+) create mode 100644 packages/fastify-dx-vue/.eslintrc create mode 100644 packages/fastify-dx-vue/README.md create mode 100644 packages/fastify-dx-vue/index.js create mode 100644 packages/fastify-dx-vue/package.json create mode 100644 packages/fastify-dx-vue/plugin.cjs create mode 100644 packages/fastify-dx-vue/server/context.js create mode 100644 packages/fastify-dx-vue/server/stream.js create mode 100644 packages/fastify-dx-vue/virtual/context.js create mode 100644 packages/fastify-dx-vue/virtual/core.jsx create mode 100644 packages/fastify-dx-vue/virtual/create.jsx create mode 100644 packages/fastify-dx-vue/virtual/layouts.js create mode 100644 packages/fastify-dx-vue/virtual/layouts/default.jsx create mode 100644 packages/fastify-dx-vue/virtual/mount.js create mode 100644 packages/fastify-dx-vue/virtual/resource.js create mode 100644 packages/fastify-dx-vue/virtual/root.jsx create mode 100644 packages/fastify-dx-vue/virtual/routes.js create mode 100644 starters/vue/.eslintignore create mode 100644 starters/vue/.eslintrc create mode 100644 starters/vue/client/assets/logo.svg create mode 100644 starters/vue/client/base.css create mode 100644 starters/vue/client/context.js create mode 100644 starters/vue/client/index.html create mode 100644 starters/vue/client/index.js create mode 100644 starters/vue/client/layouts/auth.jsx create mode 100644 starters/vue/client/layouts/default.jsx create mode 100644 starters/vue/client/pages/client-only.jsx create mode 100644 starters/vue/client/pages/index.jsx create mode 100644 starters/vue/client/pages/server-only.jsx create mode 100644 starters/vue/client/pages/streaming.jsx create mode 100644 starters/vue/client/pages/using-auth.jsx create mode 100644 starters/vue/client/pages/using-data.jsx create mode 100644 starters/vue/client/pages/using-store.jsx create mode 100644 starters/vue/client/root.jsx create mode 100644 starters/vue/package.json create mode 100644 starters/vue/postcss.config.cjs create mode 100644 starters/vue/server.js create mode 100644 starters/vue/vite.config.js diff --git a/packages/fastify-dx-vue/.eslintrc b/packages/fastify-dx-vue/.eslintrc new file mode 100644 index 0000000..2ade438 --- /dev/null +++ b/packages/fastify-dx-vue/.eslintrc @@ -0,0 +1,32 @@ +{ + parser: '@babel/eslint-parser', + parserOptions: { + requireConfigFile: false, + ecmaVersion: 2021, + sourceType: 'module', + babelOptions: { + presets: ['@babel/preset-react'], + }, + ecmaFeatures: { + jsx: true, + }, + }, + extends: [ + 'plugin:react/recommended', + 'standard', + ], + plugins: [ + 'react', + ], + rules: { + 'react/prop-types': 'off', + 'react/react-in-jsx-scope': 'off', + 'comma-dangle': ['error', 'always-multiline'], + 'import/no-absolute-path': 'off', + }, + settings: { + react: { + version: '18.0', + }, + }, +} diff --git a/packages/fastify-dx-vue/README.md b/packages/fastify-dx-vue/README.md new file mode 100644 index 0000000..8ae8e6f --- /dev/null +++ b/packages/fastify-dx-vue/README.md @@ -0,0 +1,90 @@ +# fastify-dx-react [![NPM version](https://img.shields.io/npm/v/fastify-dx-react.svg?style=flat)](https://www.npmjs.com/package/fastify-dx-react) [![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat)](https://standardjs.com/) + +- [**Introduction**](https://github.com/fastify/fastify-dx/blob/main/packages/fastify-dx-react/README.md#introduction) +- [**Quick Start**](https://github.com/fastify/fastify-dx/blob/main/packages/fastify-dx-react/README.md#quick-start) +- [**Package Scripts**](https://github.com/fastify/fastify-dx/blob/main/packages/fastify-dx-react/README.md#package-scripts) +- [**Basic Setup**](https://github.com/fastify/fastify-dx/blob/main/docs/react/basic-setup.md) +- [**Project Structure**](https://github.com/fastify/fastify-dx/blob/main/docs/react/project-structure.md) +- [**Rendering Modes**](https://github.com/fastify/fastify-dx/blob/main/docs/react/rendering-modes.md) +- [**Routing Configuration**](https://github.com/fastify/fastify-dx/blob/main/docs/react/routing-config.md) +- [**Data Prefetching**](https://github.com/fastify/fastify-dx/blob/main/docs/react/data-prefetching.md) +- [**Route Layouts**](https://github.com/fastify/fastify-dx/blob/main/docs/react/route-layouts.md) +- [**Route Context**](https://github.com/fastify/fastify-dx/blob/main/docs/react/route-context.md) +- [**Route Enter Event**](https://github.com/fastify/fastify-dx/blob/main/docs/react/route-enter.md) +- [**Virtual Modules**](https://github.com/fastify/fastify-dx/blob/main/docs/react/virtual-modules.md) + +## Introduction + +**Fastify DX for React** is a renderer adapter for [**fastify-vite**](https://github.com/fastify/fastify-vite). + +It is a **fast**, **lightweight** alternative to Next.js and Remix packed with **Developer Experience** features. + +It has an extremely small core (~1k LOC total) and is built on top of [Fastify](https://github.com/fastify/fastify), [Vite](https://vitejs.dev/), [React Router](https://reactrouter.com/docs/en/v6) and [Valtio](https://github.com/pmndrs/valtio). + +[**See the release notes for the 0.0.1 alpha release**](https://github.com/fastify/fastify-dx/releases/tag/v0.0.1). + +> At this stage this project is mostly a [**one-man show**](https://github.com/sponsors/galvez), who's devoting all his free time to its completion. Contributions are extremely welcome, as well as bug reports for any issues you may find. + +In this first alpha release it's still missing a test suite. The same is true for [**fastify-vite**](). + +It'll move into **beta** status when test suites are added to both packages. + +## Quick Start + +Ensure you have **Node v16+**. + +Make a copy of [**starters/react**](https://github.com/fastify/fastify-dx/tree/dev/starters/react). If you have [`degit`](https://github.com/Rich-Harris/degit), run the following from a new directory: + +```bash +degit fastify/fastify-dx/starters/react +``` + +> **If you're starting a project from scratch**, you'll need these packages installed. +> +> ```bash +> npm i fastify fastify-vite fastify-dx-react -P +> npm i @vitejs/plugin-react -D +> ``` + + +Run `npm install`. + +Run `npm run dev`. + +Visit `http://localhost:3000/`. + +## What's Included + +That will get you a **starter template** with: + +- A minimal [Fastify](https://github.com/fastify/fastify) server. +- Some dummy API routes. +- A `pages/` folder with some [demo routes](https://github.com/fastify/fastify-dx/tree/dev/starters/react/client/pages). +- All configuration files. + +It also includes some _**opinionated**_ essentials: + +- [**PostCSS Preset Env**](https://www.npmjs.com/package/postcss-preset-env) by [**Jonathan Neal**](https://github.com/jonathantneal), which enables [several modern CSS features](https://preset-env.cssdb.org/), such as [**CSS Nesting**](https://www.w3.org/TR/css-nesting-1/). + +- [**UnoCSS**](https://github.com/unocss/unocss) by [**Anthony Fu**](https://antfu.me/), which supports all [Tailwind utilities](https://uno.antfu.me/) and many other goodies through its [default preset](https://github.com/unocss/unocss/tree/main/packages/preset-uno). + +- [**Valtio**](https://github.com/pmndrs/valtio) by [**Daishi Kato**](https://blog.axlight.com/), with a global and SSR-ready store which you can use anywhere. + + +## Package Scripts + +`npm run dev` boots the development server. + +`npm run build` creates the production bundle. + +`npm run serve` serves the production bundle. + +## Meta + +Created by [Jonas Galvez](https://github.com/sponsors/galvez), **Engineering Manager** and **Open Sourcerer** at [NearForm](https://nearform.com). + +## Sponsors + + + +Also [**Duc-Thien Bui**](https://github.com/aecea) and [**Tom Preston-Werner**](https://github.com/mojombo) [via GitHub Sponsors](https://github.com/sponsors/galvez). _Thank you!_ diff --git a/packages/fastify-dx-vue/index.js b/packages/fastify-dx-vue/index.js new file mode 100644 index 0000000..e6673cc --- /dev/null +++ b/packages/fastify-dx-vue/index.js @@ -0,0 +1,177 @@ +// Used to send a readable stream to reply.send() +import { Readable } from 'stream' + +// fastify-vite's minimal HTML templating function, +// which extracts interpolation variables from comments +// and returns a function with the generated code +import { createHtmlTemplateFunction } from 'fastify-vite' + +// Used to safely serialize JavaScript into +// ' + ) + } + // Render page-level elements + const head = new Head(context.head).render() + // Create readable stream with prepended and appended chunks + const readable = Readable.from(generateHtmlStream({ + body: body && ( + context.streaming + ? onShellReady(body) + : onAllReady(body) + ), + head: headTemplate({ ...context, head, hydration }), + footer: footerTemplate(context), + })) + // Send out header and readable stream with full response + this.type('text/html') + this.send(readable) + } +} + +export async function createRenderFunction ({ routes, create }) { + // create is exported by client/index.js + return function (req) { + // Create convenience-access routeMap + const routeMap = Object.fromEntries(routes.toJSON().map((route) => { + return [route.path, route] + })) + // Creates main React component with all the SSR context it needs + const app = !req.route.clientOnly && create({ + routes, + routeMap, + ctxHydration: req.route, + url: req.url, + }) + // Perform SSR, i.e., turn app.instance into an HTML fragment + // The SSR context data is passed along so it can be inlined for hydration + return { routes, context: req.route, body: app } + } +} + +export function createRouteHandler (client, scope, config) { + return function (req, reply) { + reply.html(reply.render(req)) + return reply + } +} + +export function createRoute ({ client, handler, errorHandler, route }, scope, config) { + const onRequest = async function onRequest (req, reply) { + req.route = await RouteContext.create( + scope, + req, + reply, + route, + client.context, + ) + } + if (route.getData) { + // If getData is provided, register JSON endpoint for it + scope.get(`/-/data${route.path}`, { + onRequest, + async handler (req, reply) { + reply.send(await route.getData(req.route)) + }, + }) + } + + // See https://github.com/fastify/fastify-dx/blob/main/URMA.md + const hasURMAHooks = Boolean( + route.getData || route.getMeta || route.onEnter, + ) + + // Extend with route context initialization module + RouteContext.extend(client.context) + + scope.get(route.path, { + onRequest, + // If either getData or onEnter are provided, + // make sure they run before the SSR route handler + ...hasURMAHooks && { + async preHandler (req, reply) { + try { + if (route.getData) { + req.route.data = await route.getData(req.route) + } + if (route.getMeta) { + req.route.head = await route.getMeta(req.route) + } + if (route.onEnter) { + if (!req.route.data) { + req.route.data = {} + } + const result = await route.onEnter(req.route) + Object.assign(req.route.data, result) + } + } catch (err) { + if (config.dev) { + console.error(err) + } + req.route.error = err + } + }, + }, + handler, + errorHandler, + ...route, + }) +} diff --git a/packages/fastify-dx-vue/package.json b/packages/fastify-dx-vue/package.json new file mode 100644 index 0000000..9b5f427 --- /dev/null +++ b/packages/fastify-dx-vue/package.json @@ -0,0 +1,48 @@ +{ + "scripts": { + "lint": "eslint . --ext .js,.jsx --fix" + }, + "type": "module", + "main": "index.js", + "name": "fastify-dx-react", + "version": "0.0.2", + "files": [ + "virtual/create.jsx", + "virtual/root.jsx", + "virtual/layouts.js", + "virtual/layouts/default.jsx", + "virtual/context.js", + "virtual/mount.js", + "virtual/resource.js", + "virtual/core.jsx", + "virtual/routes.js", + "index.js", + "plugin.cjs", + "server/context.js", + "server/stream.js" + ], + "license": "MIT", + "exports": { + ".": "./index.js", + "./plugin": "./plugin.cjs" + }, + "dependencies": { + "react": "^18.1.0", + "react-dom": "^18.1.0", + "react-router-dom": "^6.3.0", + "devalue": "^2.0.1", + "unihead": "^0.0.6", + "valtio": "^1.6.1" + }, + "devDependencies": { + "@babel/eslint-parser": "^7.16.0", + "@babel/preset-react": "^7.16.0", + "@vitejs/plugin-react": "^1.3.2", + "eslint": "^7.32.0", + "eslint-config-standard": "^16.0.2", + "eslint-plugin-import": "^2.22.1", + "eslint-plugin-node": "^11.1.0", + "eslint-plugin-promise": "^4.3.1", + "eslint-plugin-react": "^7.29.4" + } +} \ No newline at end of file diff --git a/packages/fastify-dx-vue/plugin.cjs b/packages/fastify-dx-vue/plugin.cjs new file mode 100644 index 0000000..2ec2063 --- /dev/null +++ b/packages/fastify-dx-vue/plugin.cjs @@ -0,0 +1,107 @@ +const { readFileSync, existsSync } = require('fs') +const { dirname, join, resolve } = require('path') +const { fileURLToPath } = require('url') + +function viteReactFastifyDX (config = {}) { + const prefix = /^\/?dx:/ + const routing = Object.assign({ + globPattern: '/pages/**/*.jsx', + paramPattern: /\[(\w+)\]/, + }, config) + const virtualRoot = resolve(__dirname, 'virtual') + const virtualModules = [ + 'mount.js', + 'resource.js', + 'routes.js', + 'layouts.js', + 'create.jsx', + 'root.jsx', + 'layouts/', + 'context.js', + 'core.jsx' + ] + virtualModules.includes = function (virtual) { + if (!virtual) { + return false + } + for (const entry of this) { + if (virtual.startsWith(entry)) { + return true + } + } + return false + } + const virtualModuleInserts = { + 'routes.js': { + $globPattern: routing.globPattern, + $paramPattern: routing.paramPattern, + } + } + + let viteProjectRoot + + function loadVirtualModuleOverride (virtual) { + if (!virtualModules.includes(virtual)) { + return + } + const overridePath = resolve(viteProjectRoot, virtual) + if (existsSync(overridePath)) { + return overridePath + } + } + + function loadVirtualModule (virtual) { + if (!virtualModules.includes(virtual)) { + return + } + let code = readFileSync(resolve(virtualRoot, virtual), 'utf8') + if (virtualModuleInserts[virtual]) { + for (const [key, value] of Object.entries(virtualModuleInserts[virtual])) { + code = code.replace(new RegExp(escapeRegExp(key), 'g'), value) + } + } + return { + code, + map: null, + } + } + + // Thanks to https://github.com/sindresorhus/escape-string-regexp/blob/main/index.js + function escapeRegExp (s) { + return s + .replace(/[|\\{}()[\]^$+*?.]/g, '\\$&') + .replace(/-/g, '\\x2d') + } + + return { + name: 'vite-plugin-react-fastify-dx', + config (config, { command }) { + if (command === 'build' && config.build?.ssr) { + config.build.rollupOptions = { + output: { + format: 'es', + }, + } + } + }, + configResolved (config) { + viteProjectRoot = config.root + }, + async resolveId (id) { + const [, virtual] = id.split(prefix) + if (virtual) { + const override = await loadVirtualModuleOverride(virtual) + if (override) { + return override + } + return id + } + }, + load (id) { + const [, virtual] = id.split(prefix) + return loadVirtualModule(virtual) + }, + } +} + +module.exports = viteReactFastifyDX diff --git a/packages/fastify-dx-vue/server/context.js b/packages/fastify-dx-vue/server/context.js new file mode 100644 index 0000000..fab9a2e --- /dev/null +++ b/packages/fastify-dx-vue/server/context.js @@ -0,0 +1,65 @@ + +const routeContextInspect = Symbol.for('nodejs.util.inspect.custom') + +export default class RouteContext { + static async create (server, req, reply, route, contextInit) { + const routeContext = new RouteContext(server, req, reply, route) + if (contextInit) { + if (contextInit.state) { + routeContext.state = contextInit.state() + } + if (contextInit.default) { + await contextInit.default(routeContext) + } + } + return routeContext + } + + constructor (server, req, reply, route) { + this.server = server + this.req = req + this.reply = reply + this.head = {} + this.state = null + this.data = route.data + this.firstRender = true + this.layout = route.layout + this.getMeta = !!route.getMeta + this.getData = !!route.getData + this.onEnter = !!route.onEnter + this.streaming = route.streaming + this.clientOnly = route.clientOnly + this.serverOnly = route.serverOnly + } + + [routeContextInspect] () { + return { + ...this, + server: { [routeContextInspect]: () => '[Server]' }, + req: { [routeContextInspect]: () => '[Request]' }, + reply: { [routeContextInspect]: () => '[Reply]' }, + } + } + + toJSON () { + return { + state: this.state, + data: this.data, + layout: this.layout, + getMeta: this.getMeta, + getData: this.getData, + onEnter: this.onEnter, + firstRender: this.firstRender, + clientOnly: this.clientOnly, + } + } +} + +RouteContext.extend = function (initial) { + const { default: _, ...extra } = initial + for (const [prop, value] of Object.entries(extra)) { + if (prop !== 'data' && prop !== 'state') { + Object.defineProperty(RouteContext.prototype, prop, value) + } + } +} diff --git a/packages/fastify-dx-vue/server/stream.js b/packages/fastify-dx-vue/server/stream.js new file mode 100644 index 0000000..ea4f94e --- /dev/null +++ b/packages/fastify-dx-vue/server/stream.js @@ -0,0 +1,53 @@ + +// Helper from the Node.js stream library to +// make it easier to work with renderToPipeableStream() +import { PassThrough } from 'stream' + +// React 18's preferred server-side rendering function, +// which enables the combination of React.lazy() and Suspense +import { renderToPipeableStream } from 'react-dom/server' + +// Helper function to prepend and append chunks the body stream +export async function * generateHtmlStream ({ head, body, footer }) { + yield head + if (body) { + for await (const chunk of await body) { + yield chunk + } + } + yield footer +} + +// Helper function to get an AsyncIterable (via PassThrough) +// from the renderToPipeableStream() onShellReady event +export function onShellReady (app) { + const duplex = new PassThrough() + return new Promise((resolve, reject) => { + try { + const pipeable = renderToPipeableStream(app, { + onShellReady () { + resolve(pipeable.pipe(duplex)) + }, + }) + } catch (error) { + resolve(error) + } + }) +} + +// Helper function to get an AsyncIterable (via PassThrough) +// from the renderToPipeableStream() onAllReady event +export function onAllReady (app) { + const duplex = new PassThrough() + return new Promise((resolve, reject) => { + try { + const pipeable = renderToPipeableStream(app, { + onAllReady () { + resolve(pipeable.pipe(duplex)) + }, + }) + } catch (error) { + resolve(error) + } + }) +} diff --git a/packages/fastify-dx-vue/virtual/context.js b/packages/fastify-dx-vue/virtual/context.js new file mode 100644 index 0000000..1e605f5 --- /dev/null +++ b/packages/fastify-dx-vue/virtual/context.js @@ -0,0 +1,4 @@ +// This file serves as a placeholder +// if no context.js file is provided + +export default () => {} diff --git a/packages/fastify-dx-vue/virtual/core.jsx b/packages/fastify-dx-vue/virtual/core.jsx new file mode 100644 index 0000000..c930a49 --- /dev/null +++ b/packages/fastify-dx-vue/virtual/core.jsx @@ -0,0 +1,140 @@ +import { createContext, useContext, useEffect } from 'react' +import { useLocation, BrowserRouter, Routes, Route } from 'react-router-dom' +import { StaticRouter } from 'react-router-dom/server.mjs' +import { createPath } from 'history' +import { proxy, useSnapshot } from 'valtio' +import { waitResource, waitFetch } from '/dx:resource.js' +import layouts from '/dx:layouts.js' + +const isServer = typeof process === 'object' + +export const Router = isServer ? StaticRouter : BrowserRouter +export const RouteContext = createContext({}) + +export function useRouteContext () { + const routeContext = useContext(RouteContext) + if (routeContext.state) { + routeContext.snapshot = useSnapshot(routeContext.state) + } + return routeContext +} + +export function DXApp ({ + url, + routes, + head, + routeMap, + ctxHydration, +}) { + return ( + + { + routes.map(({ path, component: Component }) => + + + + } />, + ) + } + + ) +} + +export function DXRoute ({ head, ctxHydration, ctx, children }) { + // If running on the server, assume all data + // functions have already ran through the preHandler hook + if (isServer) { + const Layout = layouts[ctxHydration.layout ?? 'default'] + return ( + + + {children} + + + ) + } + // Note that on the client, window.route === ctxHydration + + // Indicates whether or not this is a first render on the client + ctx.firstRender = window.route.firstRender + + // If running on the client, the server context data + // is still available, hydrated from window.route + if (ctx.firstRender) { + ctx.data = window.route.data + ctx.head = window.route.head + } + + const location = useLocation() + const path = createPath(location) + + // When the next route renders client-side, + // force it to execute all URMA hooks again + useEffect(() => { + window.route.firstRender = false + }, [location]) + + // If we have a getData function registered for this route + if (!ctx.data && ctx.getData) { + try { + const { pathname, search } = location + // If not, fetch data from the JSON endpoint + ctx.data = waitFetch(`${pathname}${search}`) + } catch (status) { + // If it's an actual error... + if (status instanceof Error) { + ctx.error = status + } + // If it's just a promise (suspended state) + throw status + } + } + + // Note that ctx.loader() at this point will resolve the + // memoized module, so there's barely any overhead + + if (!ctx.firstRender && ctx.getMeta) { + const updateMeta = async () => { + const { getMeta } = await ctx.loader() + head.update(await getMeta(ctx)) + } + waitResource(path, 'updateMeta', updateMeta) + } + + if (!ctx.firstRender && ctx.onEnter) { + const runOnEnter = async () => { + const { onEnter } = await ctx.loader() + const updatedData = await onEnter(ctx) + if (!ctx.data) { + ctx.data = {} + } + Object.assign(ctx.data, updatedData) + } + waitResource(path, 'onEnter', runOnEnter) + } + + const Layout = layouts[ctx.layout ?? 'default'] + + return ( + + + {children} + + + ) +} diff --git a/packages/fastify-dx-vue/virtual/create.jsx b/packages/fastify-dx-vue/virtual/create.jsx new file mode 100644 index 0000000..cf222b2 --- /dev/null +++ b/packages/fastify-dx-vue/virtual/create.jsx @@ -0,0 +1,7 @@ +import Root from '/dx:root.jsx' + +export default function create ({ url, ...serverInit }) { + return ( + + ) +} diff --git a/packages/fastify-dx-vue/virtual/layouts.js b/packages/fastify-dx-vue/virtual/layouts.js new file mode 100644 index 0000000..3a065ab --- /dev/null +++ b/packages/fastify-dx-vue/virtual/layouts.js @@ -0,0 +1,14 @@ +import { lazy } from 'react' + +const DefaultLayout = () => import('/dx:layouts/default.jsx') + +const appLayouts = import.meta.glob('/layouts/*.jsx') + +appLayouts['/layouts/default.jsx'] ??= DefaultLayout + +export default Object.fromEntries( + Object.keys(appLayouts).map((path) => { + const name = path.slice(9, -4) + return [name, lazy(appLayouts[path])] + }), +) diff --git a/packages/fastify-dx-vue/virtual/layouts/default.jsx b/packages/fastify-dx-vue/virtual/layouts/default.jsx new file mode 100644 index 0000000..aecd273 --- /dev/null +++ b/packages/fastify-dx-vue/virtual/layouts/default.jsx @@ -0,0 +1,12 @@ +// This file serves as a placeholder +// if no layout.jsx file is provided + +import { Suspense } from 'react' + +export default function Layout ({ children }) { + return ( + + {children} + + ) +} diff --git a/packages/fastify-dx-vue/virtual/mount.js b/packages/fastify-dx-vue/virtual/mount.js new file mode 100644 index 0000000..a7c10d8 --- /dev/null +++ b/packages/fastify-dx-vue/virtual/mount.js @@ -0,0 +1,47 @@ +import Head from 'unihead/client' +import { createRoot, hydrateRoot } from 'react-dom/client' + +import create from '/dx:create.jsx' +import routesPromise from '/dx:routes.js' + +mount('main') + +async function mount (target) { + if (typeof target === 'string') { + target = document.querySelector(target) + } + const context = await import('/dx:context.js') + const ctxHydration = await extendContext(window.route, context) + const head = new Head(window.route.head, window.document) + const resolvedRoutes = await routesPromise + const routeMap = Object.fromEntries( + resolvedRoutes.map((route) => [route.path, route]), + ) + + const app = create({ + head, + ctxHydration, + routes: window.routes, + routeMap, + }) + if (ctxHydration.clientOnly) { + createRoot(target).render(app) + } else { + hydrateRoot(target, app) + } +} + +async function extendContext (ctx, { + // The route context initialization function + default: setter, + // We destructure state here just to discard it from extra + state, + // Other named exports from context.js + ...extra +}) { + Object.assign(ctx, extra) + if (setter) { + await setter(ctx) + } + return ctx +} diff --git a/packages/fastify-dx-vue/virtual/resource.js b/packages/fastify-dx-vue/virtual/resource.js new file mode 100644 index 0000000..69d7310 --- /dev/null +++ b/packages/fastify-dx-vue/virtual/resource.js @@ -0,0 +1,68 @@ + +const fetchMap = new Map() +const resourceMap = new Map() + +export function waitResource (path, id, promise) { + const resourceId = `${path}:${id}` + const loader = resourceMap.get(resourceId) + if (loader) { + if (loader.error) { + throw loader.error + } + if (loader.suspended) { + throw loader.promise + } + resourceMap.delete(resourceId) + + return loader.result + } else { + const loader = { + suspended: true, + error: null, + result: null, + promise: null, + } + loader.promise = promise() + .then((result) => { loader.result = result }) + .catch((loaderError) => { loader.error = loaderError }) + .finally(() => { loader.suspended = false }) + + resourceMap.set(resourceId, loader) + + return waitResource(path, id) + } +} + +export function waitFetch (path) { + const loader = fetchMap.get(path) + if (loader) { + if (loader.error || loader.data?.statusCode === 500) { + if (loader.data?.statusCode === 500) { + throw new Error(loader.data.message) + } + throw loader.error + } + if (loader.suspended) { + throw loader.promise + } + fetchMap.delete(path) + + return loader.data + } else { + const loader = { + suspended: true, + error: null, + data: null, + promise: null, + } + loader.promise = fetch(`/-/data${path}`) + .then((response) => response.json()) + .then((loaderData) => { loader.data = loaderData }) + .catch((loaderError) => { loader.error = loaderError }) + .finally(() => { loader.suspended = false }) + + fetchMap.set(path, loader) + + return waitFetch(path) + } +} diff --git a/packages/fastify-dx-vue/virtual/root.jsx b/packages/fastify-dx-vue/virtual/root.jsx new file mode 100644 index 0000000..f3e51a3 --- /dev/null +++ b/packages/fastify-dx-vue/virtual/root.jsx @@ -0,0 +1,10 @@ +import { Suspense } from 'react' +import { DXApp } from '/dx:core.jsx' + +export default function Root ({ url, serverInit }) { + return ( + + + + ) +} diff --git a/packages/fastify-dx-vue/virtual/routes.js b/packages/fastify-dx-vue/virtual/routes.js new file mode 100644 index 0000000..4fc95a0 --- /dev/null +++ b/packages/fastify-dx-vue/virtual/routes.js @@ -0,0 +1,121 @@ +/* global $paramPattern */ + +import { lazy } from 'react' + +export default import.meta.env.SSR + ? createRoutes(import.meta.globEager('$globPattern')) + : hydrateRoutes(import.meta.glob('$globPattern')) + +async function createRoutes (from, { param } = { param: $paramPattern }) { + // Otherwise we get a ReferenceError, but since + // this function is only ran once, there's no overhead + class Routes extends Array { + toJSON () { + return this.map((route) => { + return { + id: route.id, + path: route.path, + layout: route.layout, + getData: !!route.getData, + getMeta: !!route.getMeta, + onEnter: !!route.onEnter, + } + }) + } + } + const importPaths = Object.keys(from) + const promises = [] + if (Array.isArray(from)) { + for (const routeDef of from) { + promises.push( + getRouteModule(routeDef.path, routeDef.component) + .then((routeModule) => { + return { + id: routeDef.path, + path: routeDef.path ?? routeModule.path, + ...routeModule, + } + }), + ) + } + } else { + // Ensure that static routes have precedence over the dynamic ones + for (const path of importPaths.sort((a, b) => a > b ? -1 : 1)) { + promises.push( + getRouteModule(path, from[path]) + .then((routeModule) => { + return { + id: path, + layout: routeModule.layout, + path: routeModule.path ?? path + // Remove /pages and .jsx extension + .slice(6, -4) + // Replace [id] with :id + .replace(param, (_, m) => `:${m}`) + // Replace '/index' with '/' + .replace(/\/index$/, '/') + // Remove trailing slashs + .replace(/.+\/+$/, ''), + ...routeModule, + } + }), + ) + } + } + return new Routes(...await Promise.all(promises)) +} + +async function hydrateRoutes (from) { + if (Array.isArray(from)) { + from = Object.fromEntries( + from.map((route) => [route.path, route]), + ) + } + return window.routes.map((route) => { + route.loader = memoImport(from[route.id]) + route.component = lazy(() => route.loader()) + return route + }) +} + +function getRouteModuleExports (routeModule) { + return { + // The Route component (default export) + component: routeModule.default, + // The Layout Route component + layout: routeModule.layout, + // Route-level hooks + getData: routeModule.getData, + getMeta: routeModule.getMeta, + onEnter: routeModule.onEnter, + // Other Route-level settings + streaming: routeModule.streaming, + clientOnly: routeModule.clientOnly, + serverOnly: routeModule.serverOnly, + } +} + +async function getRouteModule (path, routeModule) { + // const isServer = typeof process !== 'undefined' + if (typeof routeModule === 'function') { + routeModule = await routeModule() + return getRouteModuleExports(routeModule) + } else { + return getRouteModuleExports(routeModule) + } +} + +function memoImport (func) { + // Otherwise we get a ReferenceError, but since this function + // is only ran once for each route, there's no overhead + const kFuncExecuted = Symbol('kFuncExecuted') + const kFuncValue = Symbol('kFuncValue') + func[kFuncExecuted] = false + return async function () { + if (!func[kFuncExecuted]) { + func[kFuncValue] = await func() + func[kFuncExecuted] = true + } + return func[kFuncValue] + } +} diff --git a/starters/vue/.eslintignore b/starters/vue/.eslintignore new file mode 100644 index 0000000..53c37a1 --- /dev/null +++ b/starters/vue/.eslintignore @@ -0,0 +1 @@ +dist \ No newline at end of file diff --git a/starters/vue/.eslintrc b/starters/vue/.eslintrc new file mode 100644 index 0000000..be9e0ff --- /dev/null +++ b/starters/vue/.eslintrc @@ -0,0 +1,31 @@ +{ + parser: '@babel/eslint-parser', + parserOptions: { + requireConfigFile: false, + ecmaVersion: 2021, + sourceType: 'module', + babelOptions: { + presets: ['@babel/preset-react'], + }, + ecmaFeatures: { + jsx: true, + }, + }, + extends: [ + 'plugin:react/recommended', + 'standard', + ], + plugins: [ + 'react', + ], + rules: { + 'comma-dangle': ['error', 'always-multiline'], + 'react/prop-types': 'off', + 'import/no-absolute-path': 'off', + }, + settings: { + react: { + version: '18.0', + }, + }, +} diff --git a/starters/vue/client/assets/logo.svg b/starters/vue/client/assets/logo.svg new file mode 100644 index 0000000..9f2f2fd --- /dev/null +++ b/starters/vue/client/assets/logo.svg @@ -0,0 +1,84 @@ + +image/svg+xml + + + + + + + + + + + + + + + + diff --git a/starters/vue/client/base.css b/starters/vue/client/base.css new file mode 100644 index 0000000..fde0cfc --- /dev/null +++ b/starters/vue/client/base.css @@ -0,0 +1,56 @@ +:root { + --color-base: #f1f1f1; + --color-highlight: #ff80ff; +} +html { + background: #222; +} +main { + width: 800px; + margin: 0 auto; + padding: 2em; + box-shadow: 5px 5px 30px rgba(0,0,0,0.4); + border-radius: 10px; + background-color: rgba(255, 255, 255, 0.1); + font-family: Avenir, Helvetica, Arial, sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + color: var(--color-base); + margin-top: 60px; + & a { + color: var(--color-highlight); + text-decoration: none; + font-weight: bold; + border-bottom: 1px solid var(--color-highlight); + &:hover { + color: #ffde00; + } + &:active { + color: #eecf00 + } + } + & p { + font-size: 1.2em; + } + & ul { + & li { + &:not(:last-child) { + margin-bottom: 0.5em; + } + break-inside: avoid; + font-size: 1em; + } + } + & code { + color: #ffde00; + font-weight: bold; + font-family: 'Consolas', 'Andale Mono', monospace; + font-size: 0.9em; + } + & img { + width: 14em; + } + & button { + margin: 0 0.5em; + } +} diff --git a/starters/vue/client/context.js b/starters/vue/client/context.js new file mode 100644 index 0000000..acb9cf7 --- /dev/null +++ b/starters/vue/client/context.js @@ -0,0 +1,36 @@ +import ky from 'ky-universal' + +export default (ctx) => { + if (ctx.server) { + ctx.state.todoList = ctx.server.db.todoList + } +} + +export const $fetch = ky.extend({ + prefixUrl: 'http://localhost:3000' +}) + +export const state = () => ({ + user: { + authenticated: false, + }, + todoList: null, +}) + +export const actions = { + authenticate (state) { + state.user.authenticated = true + }, + async addTodoItem (state, item) { + await $fetch.put('api/todo/items', { + json: { item }, + }) + state.todoList.push(item) + }, + async removeTodoItem (state, index) { + await $fetch.delete('api/todo/items', { + json: { index }, + }) + state.todoList.splice(index, 1) + } +} diff --git a/starters/vue/client/index.html b/starters/vue/client/index.html new file mode 100644 index 0000000..19bcdb9 --- /dev/null +++ b/starters/vue/client/index.html @@ -0,0 +1,13 @@ + + + + + + + + + +
+ + + diff --git a/starters/vue/client/index.js b/starters/vue/client/index.js new file mode 100644 index 0000000..425bf49 --- /dev/null +++ b/starters/vue/client/index.js @@ -0,0 +1,8 @@ +import routes from '/dx:routes.js' +import create from '/dx:create.jsx' + +export default { + context: import('/dx:context.js'), + routes, + create, +} diff --git a/starters/vue/client/layouts/auth.jsx b/starters/vue/client/layouts/auth.jsx new file mode 100644 index 0000000..517989a --- /dev/null +++ b/starters/vue/client/layouts/auth.jsx @@ -0,0 +1,25 @@ +import { Suspense } from 'react' +import { useRouteContext } from '/dx:core.jsx' + +export default function Auth ({ children }) { + const { actions, state, snapshot } = useRouteContext() + const authenticate = () => actions.authenticate(state) + return ( + + {snapshot.user.authenticated + ? children + : authenticate()} /> } + + ) +} + +function Login ({ onClick }) { + return ( + <> +

This route needs authentication.

+ + + ) +} \ No newline at end of file diff --git a/starters/vue/client/layouts/default.jsx b/starters/vue/client/layouts/default.jsx new file mode 100644 index 0000000..604b629 --- /dev/null +++ b/starters/vue/client/layouts/default.jsx @@ -0,0 +1,9 @@ +import { Suspense } from 'react' + +export default function Default ({ children }) { + return ( + + {children} + + ) +} diff --git a/starters/vue/client/pages/client-only.jsx b/starters/vue/client/pages/client-only.jsx new file mode 100644 index 0000000..b55bf1d --- /dev/null +++ b/starters/vue/client/pages/client-only.jsx @@ -0,0 +1,23 @@ +import { Link } from 'react-router-dom' + +export const clientOnly = true + +export function getMeta () { + return { + title: 'Client Only Page' + } +} + +export default function ClientOnly () { + return ( + <> +

This route is rendered on the client only!

+

+ Go back to the index +

+

+

When this route is rendered on the server, no SSR takes place.

+

See the output of curl http://localhost:3000/client-only.

+ + ) +} diff --git a/starters/vue/client/pages/index.jsx b/starters/vue/client/pages/index.jsx new file mode 100644 index 0000000..2b36de5 --- /dev/null +++ b/starters/vue/client/pages/index.jsx @@ -0,0 +1,35 @@ +import logo from '/assets/logo.svg' +import { Link } from 'react-router-dom' + +export function getMeta () { + return { + title: 'Welcome to Fastify DX!' + } +} + +export default function Index () { + return ( + <> + +

Welcome to Fastify DX for React!

+
    +
  • /using-data demonstrates how to + leverage the getData() function + and useRouteContext() to retrieve server data for a route.
  • +
  • /using-store demonstrates how to + leverage the + automated Valtio store + to retrieve server data for a route and maintain it in a global + state even after navigating to another route.
  • +
  • /using-auth demonstrates how to + wrap a route in a custom layout component.
  • +
  • /client-only demonstrates how to set + up a route for rendering on the client only (disables SSR).
  • +
  • /server-only demonstrates how to set + up a route for rendering on the server only (sends no JavaScript).
  • +
  • /streaming demonstrates how to set + up a route for SSR in streaming mode.
  • +
+ + ) +} diff --git a/starters/vue/client/pages/server-only.jsx b/starters/vue/client/pages/server-only.jsx new file mode 100644 index 0000000..bb65b74 --- /dev/null +++ b/starters/vue/client/pages/server-only.jsx @@ -0,0 +1,17 @@ +import { Link } from 'react-router-dom' + +export const serverOnly = true + +export default function ServerOnly () { + return ( + <> +

This route is rendered on the server only!

+

+ Go back to the index +

+

+

When this route is rendered on the server, no JavaScript is sent to the client.

+

See the output of curl http://localhost:3000/server-only.

+ + ) +} diff --git a/starters/vue/client/pages/streaming.jsx b/starters/vue/client/pages/streaming.jsx new file mode 100644 index 0000000..99da054 --- /dev/null +++ b/starters/vue/client/pages/streaming.jsx @@ -0,0 +1,46 @@ +import { Suspense } from 'react' + +export const streaming = true + +export default function Index () { + return ( + Waiting for content

}> + +
+ ) +} + +function Message () { + const message = afterSeconds({ + id: 'index', + message: 'Delayed by Suspense API', + seconds: 5 + }) + return

{message}

+} + +const delays = new Map() + +function afterSeconds ({ id, message, seconds }) { + const delay = delays.get(id) + if (delay) { + if (delay.message) { + delays.delete(id) + return delay.message + } + if (delay.promise) { + throw delay.promise + } + } else { + delays.set(id, { + message: null, + promise: new Promise((resolve) => { + setTimeout(() => { + delays.get(id).message = message + resolve() + }, seconds * 1000) + }) + }) + return afterSeconds({ id, message }) + } +} diff --git a/starters/vue/client/pages/using-auth.jsx b/starters/vue/client/pages/using-auth.jsx new file mode 100644 index 0000000..75e7412 --- /dev/null +++ b/starters/vue/client/pages/using-auth.jsx @@ -0,0 +1,39 @@ +import { useState } from 'react' +import { Link } from 'react-router-dom' +import { useRouteContext } from '/dx:core.jsx' + +export const layout = 'auth' + +export function getMeta () { + return { title: 'Using Custom Layout' } +} + +export default function Index (props) { + const {snapshot, state, actions} = useRouteContext() + const [input, setInput] = useState(null) + const addItem = async (value) => { + await actions.addTodoItem(state, value) + input.value = '' + } + return ( + <> +

Todo List — Using Custom Layout

+
    { + snapshot.todoList.map((item, i) => { + return
  • {item}
  • + }) + }
+
+ + +
+

+ Go back to the index +

+

+

This example is exactly the same as /using-store, + except it's wrapped in a custom layout which blocks it until + user.authenticated is true in the global state.

+ + ) +} diff --git a/starters/vue/client/pages/using-data.jsx b/starters/vue/client/pages/using-data.jsx new file mode 100644 index 0000000..b380e85 --- /dev/null +++ b/starters/vue/client/pages/using-data.jsx @@ -0,0 +1,46 @@ +import { useState } from 'react' +import { Link } from 'react-router-dom' +import { useRouteContext } from '/dx:core.jsx' + +export function getMeta () { + return { title: 'Todo List — Using Data' } +} + +export function getData ({ server }) { + return { + todoList: server.db.todoList + } +} + +export default function Index (props) { + const {data} = useRouteContext() + const [todoList, updateTodoList] = useState(data.todoList) + const [input, setInput] = useState(null) + const addItem = (value) => { + updateTodoList(list => [...list, value]) + input.value = '' + } + return ( + <> +

Todo List — Using Data

+
    { + todoList.map((item, i) => { + return
  • {item}
  • + }) + }
+
+ + +
+

+ Go back to the index +

+

+

When you navigate away from this route, any additions to the to-do + list will be lost, because they're bound to this route component only.

+

See the /using-store example to learn + how to use the application global state for it. +

+ + ) +} diff --git a/starters/vue/client/pages/using-store.jsx b/starters/vue/client/pages/using-store.jsx new file mode 100644 index 0000000..b530216 --- /dev/null +++ b/starters/vue/client/pages/using-store.jsx @@ -0,0 +1,36 @@ +import { useState } from 'react' +import { Link } from 'react-router-dom' +import { useRouteContext } from '/dx:core.jsx' + +export function getMeta () { + return { title: 'Todo List — Using Store' } +} + +export default function Index (props) { + const {snapshot, state, actions} = useRouteContext() + const [input, setInput] = useState(null) + const addItem = async (value) => { + await actions.addTodoItem(state, value) + input.value = '' + } + return ( + <> +

Todo List — Using Store

+
    { + snapshot.todoList.map((item, i) => { + return
  • {item}
  • + }) + }
+
+ + +
+

+ Go back to the index +

+

+

When you navigate away from this route, any additions to the to-do + list are not lost, because they're bound to the global application state.

+ + ) +} diff --git a/starters/vue/client/root.jsx b/starters/vue/client/root.jsx new file mode 100644 index 0000000..00cef47 --- /dev/null +++ b/starters/vue/client/root.jsx @@ -0,0 +1,11 @@ +import 'virtual:uno.css' +import { Suspense } from 'react' +import { DXApp } from '/dx:core.jsx' + +export default function Root ({ url, serverInit }) { + return ( + + + + ) +} diff --git a/starters/vue/package.json b/starters/vue/package.json new file mode 100644 index 0000000..7eeff14 --- /dev/null +++ b/starters/vue/package.json @@ -0,0 +1,45 @@ +{ + "type": "module", + "scripts": { + "dev": "node server.js --dev", + "build": "npm run build:client && npm run build:server", + "serve": "node server.js", + "devinstall": "zx ../../devinstall.mjs react -- node server.js --dev", + "build:client": "vite build --outDir dist/client --ssrManifest", + "build:server": "vite build --outDir dist/server --ssr /index.js", + "lint": "eslint . --ext .js,.jsx --fix" + }, + "dependencies": { + "fastify-dx-react": "^0.0.2", + "fastify-vite": "^3.0.0-beta.23", + "ky-universal": "^0.10.1" + }, + "distDependencies": { + "fastify-dx-react": "^0.0.2", + "fastify-vite": "^3.0.0-beta.23", + "ky-universal": "^0.10.1" + }, + "devDependencies": { + "@babel/eslint-parser": "^7.16.0", + "@babel/preset-react": "^7.16.0", + "@vitejs/plugin-react": "^1.3.2", + "eslint": "^7.32.0", + "eslint-config-standard": "^16.0.2", + "eslint-plugin-import": "^2.22.1", + "eslint-plugin-node": "^11.1.0", + "eslint-plugin-promise": "^4.3.1", + "eslint-plugin-react": "^7.29.4", + "postcss-preset-env": "^7.7.1", + "unocss": "^0.37.4", + "vite-plugin-blueprint": "^0.0.4" + }, + "devInstall": { + "local": { + "fastify-dx-react": "^0.0.1-alpha.0" + }, + "external": { + "fastify-vite": "^3.0.0-beta.23", + "ky-universal": "^0.10.1" + } + } +} \ No newline at end of file diff --git a/starters/vue/postcss.config.cjs b/starters/vue/postcss.config.cjs new file mode 100644 index 0000000..8b78078 --- /dev/null +++ b/starters/vue/postcss.config.cjs @@ -0,0 +1,9 @@ +const postcssPresetEnv = require('postcss-preset-env') + +module.exports = { + plugins: [ + postcssPresetEnv({ + stage: 1, + }), + ] +} diff --git a/starters/vue/server.js b/starters/vue/server.js new file mode 100644 index 0000000..00c8744 --- /dev/null +++ b/starters/vue/server.js @@ -0,0 +1,32 @@ +import Fastify from 'fastify' +import FastifyVite from 'fastify-vite' +import FastifyDXReact from 'fastify-dx-react' + +const server = Fastify() + +server.decorate('db', { + todoList: [ + 'Do laundry', + 'Respond to emails', + 'Write report', + ] +}) + +server.put('/api/todo/items', (req, reply) => { + server.db.todoList.push(req.body.item) + reply.send({ ok: true }) +}) + +server.delete('/api/todo/items', (req, reply) => { + server.db.todoList.splice(req.body.index, 1) + reply.send({ ok: true }) +}) + +await server.register(FastifyVite, { + root: import.meta.url, + renderer: FastifyDXReact, +}) + +await server.vite.ready() + +await server.listen(3000) diff --git a/starters/vue/vite.config.js b/starters/vue/vite.config.js new file mode 100644 index 0000000..7a22ee3 --- /dev/null +++ b/starters/vue/vite.config.js @@ -0,0 +1,29 @@ +import { join, dirname } from 'path' +import { fileURLToPath } from 'url' + +import viteReact from '@vitejs/plugin-react' +import viteReactFastifyDX from 'fastify-dx-react/plugin' +import unocss from 'unocss/vite' + +const path = fileURLToPath(import.meta.url) + +const root = join(dirname(path), 'client') +const plugins = [ + viteReact({ + // Necessary until this Vite issue is resolved: + // https://github.com/vitejs/vite/issues/3301#issuecomment-1080292430 + fastRefresh: false, + }), + viteReactFastifyDX(), + unocss() +] + +export default { + root, + plugins, + ssr: { + external: [ + 'use-sync-external-store' + ] + }, +} From 67161d47ac496ac83374963c8fa5c430717734a2 Mon Sep 17 00:00:00 2001 From: Jonas Galvez Date: Sun, 19 Jun 2022 17:04:10 -0300 Subject: [PATCH 03/14] from React to Vue --- packages/fastify-dx-vue/.eslintrc | 24 +-- packages/fastify-dx-vue/index.js | 45 +++--- packages/fastify-dx-vue/package.json | 26 ++-- packages/fastify-dx-vue/plugin.cjs | 16 +- packages/fastify-dx-vue/server/stream.js | 49 +----- packages/fastify-dx-vue/virtual/core.js | 84 +++++++++++ packages/fastify-dx-vue/virtual/core.jsx | 140 ------------------ packages/fastify-dx-vue/virtual/create.js | 38 +++++ packages/fastify-dx-vue/virtual/create.jsx | 7 - packages/fastify-dx-vue/virtual/layout.vue | 24 +++ packages/fastify-dx-vue/virtual/layouts.js | 14 -- .../virtual/layouts/default.jsx | 12 -- .../virtual/layouts/default.vue | 10 ++ packages/fastify-dx-vue/virtual/mount.js | 19 +-- packages/fastify-dx-vue/virtual/resource.js | 68 --------- packages/fastify-dx-vue/virtual/root.jsx | 10 -- packages/fastify-dx-vue/virtual/root.vue | 19 +++ packages/fastify-dx-vue/virtual/routes.js | 5 +- starters/vue/client/components/Message.vue | 26 ++++ starters/vue/client/index.js | 4 +- starters/vue/client/layouts/auth.jsx | 25 ---- starters/vue/client/layouts/auth.vue | 25 ++++ starters/vue/client/layouts/default.jsx | 9 -- starters/vue/client/layouts/default.vue | 10 ++ starters/vue/client/pages/client-only.jsx | 23 --- starters/vue/client/pages/client-only.vue | 19 +++ starters/vue/client/pages/index.jsx | 35 ----- starters/vue/client/pages/index.vue | 31 ++++ starters/vue/client/pages/server-only.jsx | 17 --- starters/vue/client/pages/server-only.vue | 13 ++ starters/vue/client/pages/streaming.jsx | 46 ------ starters/vue/client/pages/streaming.vue | 15 ++ starters/vue/client/pages/using-auth.jsx | 39 ----- starters/vue/client/pages/using-auth.vue | 44 ++++++ starters/vue/client/pages/using-data.jsx | 46 ------ starters/vue/client/pages/using-data.vue | 52 +++++++ starters/vue/client/pages/using-store.jsx | 36 ----- starters/vue/client/pages/using-store.vue | 41 +++++ starters/vue/client/root.jsx | 11 -- starters/vue/client/root.vue | 23 +++ starters/vue/package.json | 25 ++-- starters/vue/server.js | 4 +- starters/vue/vite.config.js | 19 +-- 43 files changed, 553 insertions(+), 695 deletions(-) create mode 100644 packages/fastify-dx-vue/virtual/core.js delete mode 100644 packages/fastify-dx-vue/virtual/core.jsx create mode 100644 packages/fastify-dx-vue/virtual/create.js delete mode 100644 packages/fastify-dx-vue/virtual/create.jsx create mode 100644 packages/fastify-dx-vue/virtual/layout.vue delete mode 100644 packages/fastify-dx-vue/virtual/layouts.js delete mode 100644 packages/fastify-dx-vue/virtual/layouts/default.jsx create mode 100644 packages/fastify-dx-vue/virtual/layouts/default.vue delete mode 100644 packages/fastify-dx-vue/virtual/resource.js delete mode 100644 packages/fastify-dx-vue/virtual/root.jsx create mode 100644 packages/fastify-dx-vue/virtual/root.vue create mode 100644 starters/vue/client/components/Message.vue delete mode 100644 starters/vue/client/layouts/auth.jsx create mode 100644 starters/vue/client/layouts/auth.vue delete mode 100644 starters/vue/client/layouts/default.jsx create mode 100644 starters/vue/client/layouts/default.vue delete mode 100644 starters/vue/client/pages/client-only.jsx create mode 100644 starters/vue/client/pages/client-only.vue delete mode 100644 starters/vue/client/pages/index.jsx create mode 100644 starters/vue/client/pages/index.vue delete mode 100644 starters/vue/client/pages/server-only.jsx create mode 100644 starters/vue/client/pages/server-only.vue delete mode 100644 starters/vue/client/pages/streaming.jsx create mode 100644 starters/vue/client/pages/streaming.vue delete mode 100644 starters/vue/client/pages/using-auth.jsx create mode 100644 starters/vue/client/pages/using-auth.vue delete mode 100644 starters/vue/client/pages/using-data.jsx create mode 100644 starters/vue/client/pages/using-data.vue delete mode 100644 starters/vue/client/pages/using-store.jsx create mode 100644 starters/vue/client/pages/using-store.vue delete mode 100644 starters/vue/client/root.jsx create mode 100644 starters/vue/client/root.vue diff --git a/packages/fastify-dx-vue/.eslintrc b/packages/fastify-dx-vue/.eslintrc index 2ade438..deae632 100644 --- a/packages/fastify-dx-vue/.eslintrc +++ b/packages/fastify-dx-vue/.eslintrc @@ -1,32 +1,10 @@ { - parser: '@babel/eslint-parser', - parserOptions: { - requireConfigFile: false, - ecmaVersion: 2021, - sourceType: 'module', - babelOptions: { - presets: ['@babel/preset-react'], - }, - ecmaFeatures: { - jsx: true, - }, - }, extends: [ - 'plugin:react/recommended', + 'plugin:vue/recommended', 'standard', ], - plugins: [ - 'react', - ], rules: { - 'react/prop-types': 'off', - 'react/react-in-jsx-scope': 'off', 'comma-dangle': ['error', 'always-multiline'], 'import/no-absolute-path': 'off', }, - settings: { - react: { - version: '18.0', - }, - }, } diff --git a/packages/fastify-dx-vue/index.js b/packages/fastify-dx-vue/index.js index e6673cc..f1319dd 100644 --- a/packages/fastify-dx-vue/index.js +++ b/packages/fastify-dx-vue/index.js @@ -6,6 +6,9 @@ import { Readable } from 'stream' // and returns a function with the generated code import { createHtmlTemplateFunction } from 'fastify-vite' +// Vue 3's SSR functions +import { renderToString, renderToNodeStream } from '@vue/server-renderer' + // Used to safely serialize JavaScript into // diff --git a/packages/fastify-dx-vue/virtual/layouts.js b/packages/fastify-dx-vue/virtual/layouts.js deleted file mode 100644 index 3a065ab..0000000 --- a/packages/fastify-dx-vue/virtual/layouts.js +++ /dev/null @@ -1,14 +0,0 @@ -import { lazy } from 'react' - -const DefaultLayout = () => import('/dx:layouts/default.jsx') - -const appLayouts = import.meta.glob('/layouts/*.jsx') - -appLayouts['/layouts/default.jsx'] ??= DefaultLayout - -export default Object.fromEntries( - Object.keys(appLayouts).map((path) => { - const name = path.slice(9, -4) - return [name, lazy(appLayouts[path])] - }), -) diff --git a/packages/fastify-dx-vue/virtual/layouts/default.jsx b/packages/fastify-dx-vue/virtual/layouts/default.jsx deleted file mode 100644 index aecd273..0000000 --- a/packages/fastify-dx-vue/virtual/layouts/default.jsx +++ /dev/null @@ -1,12 +0,0 @@ -// This file serves as a placeholder -// if no layout.jsx file is provided - -import { Suspense } from 'react' - -export default function Layout ({ children }) { - return ( - - {children} - - ) -} diff --git a/packages/fastify-dx-vue/virtual/layouts/default.vue b/packages/fastify-dx-vue/virtual/layouts/default.vue new file mode 100644 index 0000000..6dc560f --- /dev/null +++ b/packages/fastify-dx-vue/virtual/layouts/default.vue @@ -0,0 +1,10 @@ + + + diff --git a/packages/fastify-dx-vue/virtual/mount.js b/packages/fastify-dx-vue/virtual/mount.js index a7c10d8..c23b8d4 100644 --- a/packages/fastify-dx-vue/virtual/mount.js +++ b/packages/fastify-dx-vue/virtual/mount.js @@ -1,34 +1,25 @@ import Head from 'unihead/client' -import { createRoot, hydrateRoot } from 'react-dom/client' - -import create from '/dx:create.jsx' +import create from '/dx:create.js' import routesPromise from '/dx:routes.js' +import * as context from '/dx:context.js' mount('main') async function mount (target) { - if (typeof target === 'string') { - target = document.querySelector(target) - } - const context = await import('/dx:context.js') const ctxHydration = await extendContext(window.route, context) const head = new Head(window.route.head, window.document) const resolvedRoutes = await routesPromise const routeMap = Object.fromEntries( resolvedRoutes.map((route) => [route.path, route]), ) - - const app = create({ + const { instance, router } = await create({ head, ctxHydration, routes: window.routes, routeMap, }) - if (ctxHydration.clientOnly) { - createRoot(target).render(app) - } else { - hydrateRoot(target, app) - } + await router.isReady() + instance.mount(target) } async function extendContext (ctx, { diff --git a/packages/fastify-dx-vue/virtual/resource.js b/packages/fastify-dx-vue/virtual/resource.js deleted file mode 100644 index 69d7310..0000000 --- a/packages/fastify-dx-vue/virtual/resource.js +++ /dev/null @@ -1,68 +0,0 @@ - -const fetchMap = new Map() -const resourceMap = new Map() - -export function waitResource (path, id, promise) { - const resourceId = `${path}:${id}` - const loader = resourceMap.get(resourceId) - if (loader) { - if (loader.error) { - throw loader.error - } - if (loader.suspended) { - throw loader.promise - } - resourceMap.delete(resourceId) - - return loader.result - } else { - const loader = { - suspended: true, - error: null, - result: null, - promise: null, - } - loader.promise = promise() - .then((result) => { loader.result = result }) - .catch((loaderError) => { loader.error = loaderError }) - .finally(() => { loader.suspended = false }) - - resourceMap.set(resourceId, loader) - - return waitResource(path, id) - } -} - -export function waitFetch (path) { - const loader = fetchMap.get(path) - if (loader) { - if (loader.error || loader.data?.statusCode === 500) { - if (loader.data?.statusCode === 500) { - throw new Error(loader.data.message) - } - throw loader.error - } - if (loader.suspended) { - throw loader.promise - } - fetchMap.delete(path) - - return loader.data - } else { - const loader = { - suspended: true, - error: null, - data: null, - promise: null, - } - loader.promise = fetch(`/-/data${path}`) - .then((response) => response.json()) - .then((loaderData) => { loader.data = loaderData }) - .catch((loaderError) => { loader.error = loaderError }) - .finally(() => { loader.suspended = false }) - - fetchMap.set(path, loader) - - return waitFetch(path) - } -} diff --git a/packages/fastify-dx-vue/virtual/root.jsx b/packages/fastify-dx-vue/virtual/root.jsx deleted file mode 100644 index f3e51a3..0000000 --- a/packages/fastify-dx-vue/virtual/root.jsx +++ /dev/null @@ -1,10 +0,0 @@ -import { Suspense } from 'react' -import { DXApp } from '/dx:core.jsx' - -export default function Root ({ url, serverInit }) { - return ( - - - - ) -} diff --git a/packages/fastify-dx-vue/virtual/root.vue b/packages/fastify-dx-vue/virtual/root.vue new file mode 100644 index 0000000..0b348a8 --- /dev/null +++ b/packages/fastify-dx-vue/virtual/root.vue @@ -0,0 +1,19 @@ + + + diff --git a/packages/fastify-dx-vue/virtual/routes.js b/packages/fastify-dx-vue/virtual/routes.js index 4fc95a0..4ec3b41 100644 --- a/packages/fastify-dx-vue/virtual/routes.js +++ b/packages/fastify-dx-vue/virtual/routes.js @@ -1,7 +1,5 @@ /* global $paramPattern */ -import { lazy } from 'react' - export default import.meta.env.SSR ? createRoutes(import.meta.globEager('$globPattern')) : hydrateRoutes(import.meta.glob('$globPattern')) @@ -73,7 +71,8 @@ async function hydrateRoutes (from) { } return window.routes.map((route) => { route.loader = memoImport(from[route.id]) - route.component = lazy(() => route.loader()) + console.log('from[route.id]', from[route.id]) + route.component = from[route.id] return route }) } diff --git a/starters/vue/client/components/Message.vue b/starters/vue/client/components/Message.vue new file mode 100644 index 0000000..fbbb145 --- /dev/null +++ b/starters/vue/client/components/Message.vue @@ -0,0 +1,26 @@ + + + \ No newline at end of file diff --git a/starters/vue/client/index.js b/starters/vue/client/index.js index 425bf49..16941c8 100644 --- a/starters/vue/client/index.js +++ b/starters/vue/client/index.js @@ -1,8 +1,8 @@ import routes from '/dx:routes.js' -import create from '/dx:create.jsx' +import create from '/dx:create.js' export default { - context: import('/dx:context.js'), + context: await import('/dx:context.js'), routes, create, } diff --git a/starters/vue/client/layouts/auth.jsx b/starters/vue/client/layouts/auth.jsx deleted file mode 100644 index 517989a..0000000 --- a/starters/vue/client/layouts/auth.jsx +++ /dev/null @@ -1,25 +0,0 @@ -import { Suspense } from 'react' -import { useRouteContext } from '/dx:core.jsx' - -export default function Auth ({ children }) { - const { actions, state, snapshot } = useRouteContext() - const authenticate = () => actions.authenticate(state) - return ( - - {snapshot.user.authenticated - ? children - : authenticate()} /> } - - ) -} - -function Login ({ onClick }) { - return ( - <> -

This route needs authentication.

- - - ) -} \ No newline at end of file diff --git a/starters/vue/client/layouts/auth.vue b/starters/vue/client/layouts/auth.vue new file mode 100644 index 0000000..ebb91c5 --- /dev/null +++ b/starters/vue/client/layouts/auth.vue @@ -0,0 +1,25 @@ + + + diff --git a/starters/vue/client/layouts/default.jsx b/starters/vue/client/layouts/default.jsx deleted file mode 100644 index 604b629..0000000 --- a/starters/vue/client/layouts/default.jsx +++ /dev/null @@ -1,9 +0,0 @@ -import { Suspense } from 'react' - -export default function Default ({ children }) { - return ( - - {children} - - ) -} diff --git a/starters/vue/client/layouts/default.vue b/starters/vue/client/layouts/default.vue new file mode 100644 index 0000000..19d7e6a --- /dev/null +++ b/starters/vue/client/layouts/default.vue @@ -0,0 +1,10 @@ + + + diff --git a/starters/vue/client/pages/client-only.jsx b/starters/vue/client/pages/client-only.jsx deleted file mode 100644 index b55bf1d..0000000 --- a/starters/vue/client/pages/client-only.jsx +++ /dev/null @@ -1,23 +0,0 @@ -import { Link } from 'react-router-dom' - -export const clientOnly = true - -export function getMeta () { - return { - title: 'Client Only Page' - } -} - -export default function ClientOnly () { - return ( - <> -

This route is rendered on the client only!

-

- Go back to the index -

-

-

When this route is rendered on the server, no SSR takes place.

-

See the output of curl http://localhost:3000/client-only.

- - ) -} diff --git a/starters/vue/client/pages/client-only.vue b/starters/vue/client/pages/client-only.vue new file mode 100644 index 0000000..f93f3c7 --- /dev/null +++ b/starters/vue/client/pages/client-only.vue @@ -0,0 +1,19 @@ + + + \ No newline at end of file diff --git a/starters/vue/client/pages/index.jsx b/starters/vue/client/pages/index.jsx deleted file mode 100644 index 2b36de5..0000000 --- a/starters/vue/client/pages/index.jsx +++ /dev/null @@ -1,35 +0,0 @@ -import logo from '/assets/logo.svg' -import { Link } from 'react-router-dom' - -export function getMeta () { - return { - title: 'Welcome to Fastify DX!' - } -} - -export default function Index () { - return ( - <> - -

Welcome to Fastify DX for React!

-
    -
  • /using-data demonstrates how to - leverage the getData() function - and useRouteContext() to retrieve server data for a route.
  • -
  • /using-store demonstrates how to - leverage the - automated Valtio store - to retrieve server data for a route and maintain it in a global - state even after navigating to another route.
  • -
  • /using-auth demonstrates how to - wrap a route in a custom layout component.
  • -
  • /client-only demonstrates how to set - up a route for rendering on the client only (disables SSR).
  • -
  • /server-only demonstrates how to set - up a route for rendering on the server only (sends no JavaScript).
  • -
  • /streaming demonstrates how to set - up a route for SSR in streaming mode.
  • -
- - ) -} diff --git a/starters/vue/client/pages/index.vue b/starters/vue/client/pages/index.vue new file mode 100644 index 0000000..3b5d6bc --- /dev/null +++ b/starters/vue/client/pages/index.vue @@ -0,0 +1,31 @@ + + + + + diff --git a/starters/vue/client/pages/server-only.jsx b/starters/vue/client/pages/server-only.jsx deleted file mode 100644 index bb65b74..0000000 --- a/starters/vue/client/pages/server-only.jsx +++ /dev/null @@ -1,17 +0,0 @@ -import { Link } from 'react-router-dom' - -export const serverOnly = true - -export default function ServerOnly () { - return ( - <> -

This route is rendered on the server only!

-

- Go back to the index -

-

-

When this route is rendered on the server, no JavaScript is sent to the client.

-

See the output of curl http://localhost:3000/server-only.

- - ) -} diff --git a/starters/vue/client/pages/server-only.vue b/starters/vue/client/pages/server-only.vue new file mode 100644 index 0000000..a4a29b4 --- /dev/null +++ b/starters/vue/client/pages/server-only.vue @@ -0,0 +1,13 @@ + + + \ No newline at end of file diff --git a/starters/vue/client/pages/streaming.jsx b/starters/vue/client/pages/streaming.jsx deleted file mode 100644 index 99da054..0000000 --- a/starters/vue/client/pages/streaming.jsx +++ /dev/null @@ -1,46 +0,0 @@ -import { Suspense } from 'react' - -export const streaming = true - -export default function Index () { - return ( - Waiting for content

}> - -
- ) -} - -function Message () { - const message = afterSeconds({ - id: 'index', - message: 'Delayed by Suspense API', - seconds: 5 - }) - return

{message}

-} - -const delays = new Map() - -function afterSeconds ({ id, message, seconds }) { - const delay = delays.get(id) - if (delay) { - if (delay.message) { - delays.delete(id) - return delay.message - } - if (delay.promise) { - throw delay.promise - } - } else { - delays.set(id, { - message: null, - promise: new Promise((resolve) => { - setTimeout(() => { - delays.get(id).message = message - resolve() - }, seconds * 1000) - }) - }) - return afterSeconds({ id, message }) - } -} diff --git a/starters/vue/client/pages/streaming.vue b/starters/vue/client/pages/streaming.vue new file mode 100644 index 0000000..b8af014 --- /dev/null +++ b/starters/vue/client/pages/streaming.vue @@ -0,0 +1,15 @@ + + + \ No newline at end of file diff --git a/starters/vue/client/pages/using-auth.jsx b/starters/vue/client/pages/using-auth.jsx deleted file mode 100644 index 75e7412..0000000 --- a/starters/vue/client/pages/using-auth.jsx +++ /dev/null @@ -1,39 +0,0 @@ -import { useState } from 'react' -import { Link } from 'react-router-dom' -import { useRouteContext } from '/dx:core.jsx' - -export const layout = 'auth' - -export function getMeta () { - return { title: 'Using Custom Layout' } -} - -export default function Index (props) { - const {snapshot, state, actions} = useRouteContext() - const [input, setInput] = useState(null) - const addItem = async (value) => { - await actions.addTodoItem(state, value) - input.value = '' - } - return ( - <> -

Todo List — Using Custom Layout

-
    { - snapshot.todoList.map((item, i) => { - return
  • {item}
  • - }) - }
-
- - -
-

- Go back to the index -

-

-

This example is exactly the same as /using-store, - except it's wrapped in a custom layout which blocks it until - user.authenticated is true in the global state.

- - ) -} diff --git a/starters/vue/client/pages/using-auth.vue b/starters/vue/client/pages/using-auth.vue new file mode 100644 index 0000000..d774e8f --- /dev/null +++ b/starters/vue/client/pages/using-auth.vue @@ -0,0 +1,44 @@ + + + diff --git a/starters/vue/client/pages/using-data.jsx b/starters/vue/client/pages/using-data.jsx deleted file mode 100644 index b380e85..0000000 --- a/starters/vue/client/pages/using-data.jsx +++ /dev/null @@ -1,46 +0,0 @@ -import { useState } from 'react' -import { Link } from 'react-router-dom' -import { useRouteContext } from '/dx:core.jsx' - -export function getMeta () { - return { title: 'Todo List — Using Data' } -} - -export function getData ({ server }) { - return { - todoList: server.db.todoList - } -} - -export default function Index (props) { - const {data} = useRouteContext() - const [todoList, updateTodoList] = useState(data.todoList) - const [input, setInput] = useState(null) - const addItem = (value) => { - updateTodoList(list => [...list, value]) - input.value = '' - } - return ( - <> -

Todo List — Using Data

-
    { - todoList.map((item, i) => { - return
  • {item}
  • - }) - }
-
- - -
-

- Go back to the index -

-

-

When you navigate away from this route, any additions to the to-do - list will be lost, because they're bound to this route component only.

-

See the /using-store example to learn - how to use the application global state for it. -

- - ) -} diff --git a/starters/vue/client/pages/using-data.vue b/starters/vue/client/pages/using-data.vue new file mode 100644 index 0000000..da05d5d --- /dev/null +++ b/starters/vue/client/pages/using-data.vue @@ -0,0 +1,52 @@ + + + diff --git a/starters/vue/client/pages/using-store.jsx b/starters/vue/client/pages/using-store.jsx deleted file mode 100644 index b530216..0000000 --- a/starters/vue/client/pages/using-store.jsx +++ /dev/null @@ -1,36 +0,0 @@ -import { useState } from 'react' -import { Link } from 'react-router-dom' -import { useRouteContext } from '/dx:core.jsx' - -export function getMeta () { - return { title: 'Todo List — Using Store' } -} - -export default function Index (props) { - const {snapshot, state, actions} = useRouteContext() - const [input, setInput] = useState(null) - const addItem = async (value) => { - await actions.addTodoItem(state, value) - input.value = '' - } - return ( - <> -

Todo List — Using Store

-
    { - snapshot.todoList.map((item, i) => { - return
  • {item}
  • - }) - }
-
- - -
-

- Go back to the index -

-

-

When you navigate away from this route, any additions to the to-do - list are not lost, because they're bound to the global application state.

- - ) -} diff --git a/starters/vue/client/pages/using-store.vue b/starters/vue/client/pages/using-store.vue new file mode 100644 index 0000000..5f65345 --- /dev/null +++ b/starters/vue/client/pages/using-store.vue @@ -0,0 +1,41 @@ + + + diff --git a/starters/vue/client/root.jsx b/starters/vue/client/root.jsx deleted file mode 100644 index 00cef47..0000000 --- a/starters/vue/client/root.jsx +++ /dev/null @@ -1,11 +0,0 @@ -import 'virtual:uno.css' -import { Suspense } from 'react' -import { DXApp } from '/dx:core.jsx' - -export default function Root ({ url, serverInit }) { - return ( - - - - ) -} diff --git a/starters/vue/client/root.vue b/starters/vue/client/root.vue new file mode 100644 index 0000000..d56013a --- /dev/null +++ b/starters/vue/client/root.vue @@ -0,0 +1,23 @@ + + + + + diff --git a/starters/vue/package.json b/starters/vue/package.json index 7eeff14..50d40e0 100644 --- a/starters/vue/package.json +++ b/starters/vue/package.json @@ -10,36 +10,33 @@ "lint": "eslint . --ext .js,.jsx --fix" }, "dependencies": { - "fastify-dx-react": "^0.0.2", "fastify-vite": "^3.0.0-beta.23", - "ky-universal": "^0.10.1" - }, - "distDependencies": { - "fastify-dx-react": "^0.0.2", - "fastify-vite": "^3.0.0-beta.23", - "ky-universal": "^0.10.1" + "ky-universal": "^0.10.1", + "@vue/server-renderer": "^3.2.33", + "devalue": "^2.0.1", + "vue-router": "^4.0.15", + "unihead": "^0.0.6" }, "devDependencies": { "@babel/eslint-parser": "^7.16.0", - "@babel/preset-react": "^7.16.0", - "@vitejs/plugin-react": "^1.3.2", + "@vitejs/plugin-vue": "^2.3.2", "eslint": "^7.32.0", "eslint-config-standard": "^16.0.2", "eslint-plugin-import": "^2.22.1", "eslint-plugin-node": "^11.1.0", "eslint-plugin-promise": "^4.3.1", - "eslint-plugin-react": "^7.29.4", + "eslint-plugin-vue": "^8.7.1", "postcss-preset-env": "^7.7.1", - "unocss": "^0.37.4", - "vite-plugin-blueprint": "^0.0.4" + "unocss": "^0.37.4" }, "devInstall": { "local": { - "fastify-dx-react": "^0.0.1-alpha.0" + "fastify-dx-vue": "^0.0.3" }, "external": { "fastify-vite": "^3.0.0-beta.23", - "ky-universal": "^0.10.1" + "ky-universal": "^0.10.1", + "@vueuse/core": "^8.7.4" } } } \ No newline at end of file diff --git a/starters/vue/server.js b/starters/vue/server.js index 00c8744..c7ae4e3 100644 --- a/starters/vue/server.js +++ b/starters/vue/server.js @@ -1,6 +1,6 @@ import Fastify from 'fastify' import FastifyVite from 'fastify-vite' -import FastifyDXReact from 'fastify-dx-react' +import FastifyDXVue from 'fastify-dx-vue' const server = Fastify() @@ -24,7 +24,7 @@ server.delete('/api/todo/items', (req, reply) => { await server.register(FastifyVite, { root: import.meta.url, - renderer: FastifyDXReact, + renderer: FastifyDXVue, }) await server.vite.ready() diff --git a/starters/vue/vite.config.js b/starters/vue/vite.config.js index 7a22ee3..5090b69 100644 --- a/starters/vue/vite.config.js +++ b/starters/vue/vite.config.js @@ -1,29 +1,20 @@ import { join, dirname } from 'path' import { fileURLToPath } from 'url' -import viteReact from '@vitejs/plugin-react' -import viteReactFastifyDX from 'fastify-dx-react/plugin' +import viteVue from '@vitejs/plugin-vue' +import viteVueFastifyDX from 'fastify-dx-vue/plugin' import unocss from 'unocss/vite' const path = fileURLToPath(import.meta.url) const root = join(dirname(path), 'client') const plugins = [ - viteReact({ - // Necessary until this Vite issue is resolved: - // https://github.com/vitejs/vite/issues/3301#issuecomment-1080292430 - fastRefresh: false, - }), - viteReactFastifyDX(), - unocss() + unocss(), + viteVue(), + viteVueFastifyDX(), ] export default { root, plugins, - ssr: { - external: [ - 'use-sync-external-store' - ] - }, } From 2ed93c255c3103bbc00deaf3ce0e868b6e322c71 Mon Sep 17 00:00:00 2001 From: Jonas Galvez Date: Sun, 19 Jun 2022 17:16:50 -0300 Subject: [PATCH 04/14] cleanups --- packages/fastify-dx-vue/virtual/routes.js | 1 - starters/vue/client/mount.js | 38 +++++++++++++++++++++++ starters/vue/client/pages/index.vue | 2 +- starters/vue/client/pages/using-data.vue | 5 ++- starters/vue/package.json | 1 + starters/vue/vite.config.js | 2 +- 6 files changed, 43 insertions(+), 6 deletions(-) create mode 100644 starters/vue/client/mount.js diff --git a/packages/fastify-dx-vue/virtual/routes.js b/packages/fastify-dx-vue/virtual/routes.js index 4ec3b41..eae15e7 100644 --- a/packages/fastify-dx-vue/virtual/routes.js +++ b/packages/fastify-dx-vue/virtual/routes.js @@ -71,7 +71,6 @@ async function hydrateRoutes (from) { } return window.routes.map((route) => { route.loader = memoImport(from[route.id]) - console.log('from[route.id]', from[route.id]) route.component = from[route.id] return route }) diff --git a/starters/vue/client/mount.js b/starters/vue/client/mount.js new file mode 100644 index 0000000..c23b8d4 --- /dev/null +++ b/starters/vue/client/mount.js @@ -0,0 +1,38 @@ +import Head from 'unihead/client' +import create from '/dx:create.js' +import routesPromise from '/dx:routes.js' +import * as context from '/dx:context.js' + +mount('main') + +async function mount (target) { + const ctxHydration = await extendContext(window.route, context) + const head = new Head(window.route.head, window.document) + const resolvedRoutes = await routesPromise + const routeMap = Object.fromEntries( + resolvedRoutes.map((route) => [route.path, route]), + ) + const { instance, router } = await create({ + head, + ctxHydration, + routes: window.routes, + routeMap, + }) + await router.isReady() + instance.mount(target) +} + +async function extendContext (ctx, { + // The route context initialization function + default: setter, + // We destructure state here just to discard it from extra + state, + // Other named exports from context.js + ...extra +}) { + Object.assign(ctx, extra) + if (setter) { + await setter(ctx) + } + return ctx +} diff --git a/starters/vue/client/pages/index.vue b/starters/vue/client/pages/index.vue index 3b5d6bc..c324257 100644 --- a/starters/vue/client/pages/index.vue +++ b/starters/vue/client/pages/index.vue @@ -6,7 +6,7 @@ leverage the getData() function and useRouteContext() to retrieve server data for a route.
  • /using-store demonstrates how to - retrieve server data and maintain it in the global state (automatically hydrated).
  • + retrieve server data and maintain it in the global state.
  • /using-auth demonstrates how to wrap a route in a custom layout component.
  • /client-only demonstrates how to set diff --git a/starters/vue/client/pages/using-data.vue b/starters/vue/client/pages/using-data.vue index da05d5d..bba270a 100644 --- a/starters/vue/client/pages/using-data.vue +++ b/starters/vue/client/pages/using-data.vue @@ -38,10 +38,9 @@ export function getData ({ server }) { export default { setup () { - const routeContext = useRouteContext() - console.log('->routeContext->', routeContext) + const { data } = useRouteContext() const inputValue = ref(null) - const todoList = reactive(routeContext.data.todoList) + const todoList = reactive(data.todoList) const addItem = () => { todoList.push(inputValue.value) inputValue.value = '' diff --git a/starters/vue/package.json b/starters/vue/package.json index 50d40e0..67fe0a1 100644 --- a/starters/vue/package.json +++ b/starters/vue/package.json @@ -12,6 +12,7 @@ "dependencies": { "fastify-vite": "^3.0.0-beta.23", "ky-universal": "^0.10.1", + "@vueuse/core": "^8.7.4", "@vue/server-renderer": "^3.2.33", "devalue": "^2.0.1", "vue-router": "^4.0.15", diff --git a/starters/vue/vite.config.js b/starters/vue/vite.config.js index 5090b69..51e4b35 100644 --- a/starters/vue/vite.config.js +++ b/starters/vue/vite.config.js @@ -9,8 +9,8 @@ const path = fileURLToPath(import.meta.url) const root = join(dirname(path), 'client') const plugins = [ - unocss(), viteVue(), + unocss(), viteVueFastifyDX(), ] From a048ee7ed905c113f38b1d34540a97459818de77 Mon Sep 17 00:00:00 2001 From: Jonas Galvez Date: Sun, 19 Jun 2022 21:29:09 -0300 Subject: [PATCH 05/14] wip vue --- packages/fastify-dx-vue/virtual/core.js | 11 +++++------ packages/fastify-dx-vue/virtual/create.js | 8 +++++--- packages/fastify-dx-vue/virtual/layout.vue | 9 ++++++--- packages/fastify-dx-vue/virtual/root.vue | 5 +---- starters/vue/client/root.vue | 5 +---- 5 files changed, 18 insertions(+), 20 deletions(-) diff --git a/packages/fastify-dx-vue/virtual/core.js b/packages/fastify-dx-vue/virtual/core.js index 39c6cbc..0e47c5b 100644 --- a/packages/fastify-dx-vue/virtual/core.js +++ b/packages/fastify-dx-vue/virtual/core.js @@ -4,6 +4,7 @@ import { useRoute, createMemoryHistory, createWebHistory } from 'vue-router' export const isServer = typeof process === 'object' export const createHistory = isServer ? createMemoryHistory : createWebHistory export const serverRouteContext = Symbol('serverRouteContext') +export const routeLayout = Symbol('routeLayout') export function useRouteContext () { if (isServer) { @@ -13,11 +14,7 @@ export function useRouteContext () { } } -export function createBeforeEachHandler ({ - routeMap, - ctxHydration, - head, -}) { +export function createBeforeEachHandler ({ routeMap, ctxHydration, head }, layout) { return async function beforeCreate (to) { // The client-side route context const ctx = routeMap[to.matched[0].path] @@ -26,7 +23,9 @@ export function createBeforeEachHandler ({ ctx.state = ctxHydration.state ctx.actions = ctxHydration.actions - ctx.layout ??= 'default' + + // Update layoutRef + layout.value = ctx.layout ?? 'default' // If it is, take server context data from hydration and return immediately if (ctx.firstRender) { diff --git a/packages/fastify-dx-vue/virtual/create.js b/packages/fastify-dx-vue/virtual/create.js index 405c5e4..0d7e2ba 100644 --- a/packages/fastify-dx-vue/virtual/create.js +++ b/packages/fastify-dx-vue/virtual/create.js @@ -1,9 +1,10 @@ -import { createApp, createSSRApp, reactive } from 'vue' +import { createApp, createSSRApp, reactive, ref } from 'vue' import { createRouter } from 'vue-router' import { isServer, createHistory, serverRouteContext, + routeLayout, createBeforeEachHandler, } from '/dx:core.js' import root from '/dx:root.vue' @@ -17,14 +18,15 @@ export default async function create (ctx) { const history = createHistory() const router = createRouter({ history, routes }) + const layoutRef = ref(ctxHydration.layout ?? 'default') - ctxHydration.layout ??= 'default' + instance.provide(routeLayout, layoutRef) ctxHydration.state = reactive(ctxHydration.state) if (isServer) { instance.provide(serverRouteContext, ctxHydration) } else { - router.beforeEach(createBeforeEachHandler(ctx)) + router.beforeEach(createBeforeEachHandler(ctx, layoutRef)) } instance.use(router) diff --git a/packages/fastify-dx-vue/virtual/layout.vue b/packages/fastify-dx-vue/virtual/layout.vue index e0d5b21..8a12b65 100644 --- a/packages/fastify-dx-vue/virtual/layout.vue +++ b/packages/fastify-dx-vue/virtual/layout.vue @@ -1,11 +1,12 @@