• Stars
    star
    149
  • Rank 248,619 (Top 5 %)
  • Language
    JavaScript
  • License
    MIT License
  • Created over 2 years ago
  • Updated about 2 years ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Ruck is an open source buildless React web application framework for Deno.

Ruck logo

Ruck

Ruck is an open source buildless React web application framework for Deno. It can be used to create basic sites or powerful apps.

Features

Work with cutting edge standard technologies such as ESM, dynamic imports, HTTP imports, and import maps to avoid build steps like transpilation or bundling. Deno and browsers directly run the source code. Ruck is extremely lean with few dependencies. Modules are focused with default exports that are only deep imported when needed, for optimal JavaScript module design.

Some things that are complicated or impossible with traditional frameworks are easy with Ruck, for example…

  • Matching dynamic routes with RegEx or custom logic. Ideally an invalid slug in a route URL results in a error page without loading the route’s components or data.
  • Components can use the TransferContext React context during SSR to read the page HTTP request and modify the response. This is surprisingly difficult with Next.js, see next-server-context.
  • Proper React rendering of head tags specified by the useHead React hook. React components with a normal lifecycle can be used to render head tags that can be grouped, ordered, and prioritized for overrides. Frameworks like Next.js provide a React component that accepts basic head tags as children that it manually iterates and syncs in the document head DOM.
  • SSR with component level data fetching. This is quite tricky with frameworks like Next.js and Remix that support data fetching at the route level, see next-graphql-react.
  • GraphQL ready to use via React hooks from graphql-react.
  • Declarative system for auto loading and unloading component CSS file dependencies with absolute or relative URLs.
  • Baked-in type safety and IntelliSense via TypeScript JSDoc comments.

Installation

A Ruck project contains:

  • A Deno config file called deno.json or deno.jsonc, containing:

    {
      "compilerOptions": {
        "lib": [
          "dom",
          "dom.iterable",
          "dom.asynciterable",
          "deno.ns",
          "deno.unstable"
        ]
      }
    }

    This enables Deno and DOM types for project and imported dependency modules.

  • Import map JSON files that tell your IDE, Deno, and browsers where to import dependencies from. Ruck automatically uses es-module-shims so you don’t need to worry about poor browser support for import maps.

    Ideally use separate development and production import maps for the server and client. This way a version of React that has more detailed error messages can be used during local development, and server specific dependencies can be excluded from the browser import map for a faster page load.

    Recommended import map file names and starter contents:

    A DRY approach is to Git ignore the import map files and generate them with a script that’s a single source of truth.

  • A module that imports and uses Ruck’s serve function to start the Ruck app server, typically called scripts/serve.mjs. Here’s an example:

    // @ts-check
    
    import serve from "ruck/serve.mjs";
    
    serve({
      clientImportMap: new URL(
        Deno.env.get("RUCK_DEV") === "true"
          ? "../importMap.client.dev.json"
          : "../importMap.client.json",
        import.meta.url,
      ),
      port: Number(Deno.env.get("RUCK_PORT")),
    });
    
    console.info(
      `Ruck app HTTP server listening on http://localhost:${
        Deno.env.get("RUCK_PORT")
      }`,
    );

    The Deno CLI is used to run this script; Ruck doesn’t have a CLI.

    You may choose to create a scripts/serve.sh shell script that serves the Ruck app:

    #!/bin/sh
    # Serves the Ruck app.
    
    # Asserts an environment variable is set.
    # Argument 1: Name.
    # Argument 2: Value.
    assertEnvVar() {
      if [ -z "$2" ]
      then
        echo "Missing environment variable \`$1\`." >&2
        exit 1
      fi
    }
    
    # Assert environment variables are set.
    assertEnvVar RUCK_DEV $RUCK_DEV
    assertEnvVar RUCK_PORT $RUCK_PORT
    
    # Serve the Ruck app.
    if [ "$RUCK_DEV" = "true" ]
    then
      deno run \
        --allow-env \
        --allow-net \
        --allow-read \
        --import-map=importMap.server.dev.json \
        --watch=. \
        scripts/serve.mjs
    else
      deno run \
        --allow-env \
        --allow-net \
        --allow-read \
        --import-map=importMap.server.json \
        --no-check \
        scripts/serve.mjs
    fi

    First, ensure it’s executable:

    chmod +x ./scripts/serve.sh

    Then, run it like this:

    RUCK_DEV="true" RUCK_PORT="3000" ./scripts/serve.sh

    You may choose to store environment variables in a Git ignored scripts/.env.sh file:

    export RUCK_DEV="true"
    export RUCK_PORT="3000"

    Then, you could create a scripts/dev.sh shell script (also ensure it’s executable):

    #!/bin/sh
    # Loads the environment variables and serves the Ruck app.
    
    # Load the environment variables.
    . scripts/.env.sh &&
    
    # Serve the Ruck app.
    ./scripts/serve.sh

    This way you only need to run this when developing your Ruck app:

    ./scripts/dev.sh

    There isn’t a universally “correct” way to use environment variables or start serving the Ruck app; create an optimal workflow for your particular development and production environments.

  • A public directory containing files that Ruck serves directly to browsers, by default called public. For example, public/favicon.ico could be accessed in a browser at the URL path /favicon.ico.

  • A router.mjs module in the public directory that default exports a function that Ruck calls on both the server and client with details such as the route URL to determine what the route content should be. It should have this JSDoc type:

    /** @type {import("ruck/serve.mjs").Router} */

    Ruck provides an (optional) declarative system for automatic loading and unloading of component CSS file dependencies served by Ruck via the public directory or CDN. Ruck’s routePlanForContentWithCss function can be imported and used to create route plan for content with CSS file dependencies.

    Here is an example for a website that has a home page, a /blog page that lists blog posts, and a /blog/post-id-slug-here page for individual blog posts:

    // @ts-check
    
    import { createElement as h } from "react";
    import routePlanForContentWithCss from "ruck/routePlanForContentWithCss.mjs";
    
    // The component used to display a route loading error (e.g. due to an
    // internet dropout) should be imported up front instead of dynamically
    // importing it when needed, as it would likely also fail to load.
    import PageError, {
      // A `Set` instance containing CSS URLs.
      css as cssPageError,
    } from "./components/PageError.mjs";
    
    /**
     * Gets the Ruck app route plan for a URL.
     * @type {import("ruck/serve.mjs").Router}
     */
    export default function router(url, headManager, isInitialRoute) {
      if (url.pathname === "/") {
        return routePlanForContentWithCss(
          // Dynamically import route components so they only load when needed.
          import("./components/PageHome.mjs").then(
            ({ default: PageHome, css }) => ({
              content: h(PageHome),
              css,
            }),
            // It’s important to handle dynamic import loading errors.
            catchImportContentWithCss,
          ),
          headManager,
          isInitialRoute,
        );
      }
    
      if (url.pathname === "/blog") {
        return routePlanForContentWithCss(
          import("./components/PageBlog.mjs").then(
            ({ default: PageBlog, css }) => ({
              content: h(PageBlog),
              css,
            }),
            catchImportContentWithCss,
          ),
          headManager,
          isInitialRoute,
        );
      }
    
      // For routes with URL slugs, use RegEx that only matches valid slugs,
      // instead of simply extracting the whole slug. This way an invalid URL slug
      // naturally results in an immediate 404 error and avoids loading the route
      // component or loading data with the invalid slug.
      const matchPagePost = url.pathname.match(/^\/blog\/(?<postId>[\w-]+)$/u);
    
      if (matchPagePost?.groups) {
        const { postId } = matchPagePost.groups;
    
        return routePlanForContentWithCss(
          import("./components/PagePost.mjs").then(
            ({ default: PagePost, css }) => ({
              content: h(PagePost, { postId }),
              css,
            }),
          ),
          headManager,
          isInitialRoute,
        );
      }
    
      // Fallback to a 404 error page.
      return routePlanForContentWithCss(
        // If you have a component specifically for a 404 error page, it would be
        // ok to dynamically import it here. In this particular example the
        // component was already imported for the loading error page.
        {
          content: h(PageError, {
            status: 404,
            title: "Error 404",
            description: "Something is missing.",
          }),
          css: cssPageError,
        },
        headManager,
        isInitialRoute,
      );
    }
    
    /**
     * Catches a dynamic import error for route content with CSS.
     * @param {Error} cause Import error.
     * @returns {import("ruck/routePlanForContentWithCss.mjs").RouteContentWithCss}
     */
    function catchImportContentWithCss(cause) {
      console.error(new Error("Import rejection for route with CSS.", { cause }));
    
      return {
        content: h(PageError, {
          status: 500,
          title: "Error loading",
          description: "Unable to load.",
        }),
        css: cssPageError,
      };
    }

    For the previous example, here’s the public/components/PageError.mjs module:

    // @ts-check
    
    import { createElement as h, useContext } from "react";
    import TransferContext from "ruck/TransferContext.mjs";
    
    import Heading, { css as cssHeading } from "./Heading.mjs";
    import Para, { css as cssPara } from "./Para.mjs";
    
    // Export CSS URLs for the component and its dependencies.
    export const css = new Set([
      ...cssHeading,
      ...cssPara,
      "/components/PageError.css",
    ]);
    
    /**
     * React component for an error page.
     * @param {object} props Props.
     * @param {number} props.status HTTP status code.
     * @param {number} props.title Error title.
     * @param {string} props.description Error description.
     */
    export default function PageError({ status, title, description }) {
      // Ruck’s transfer (request/response) context; only populated on the server.
      const ruckTransfer = useContext(TransferContext);
    
      // If server side rendering, modify the HTTP status code for the Ruck app
      // page response.
      if (ruckTransfer) ruckTransfer.responseInit.status = status;
    
      return h(
        "section",
        { className: "PageError__section" },
        h(Heading, null, title),
        h(Para, null, description),
      );
    }
  • A components/App.mjs module in the public directory that default exports a React component that renders the entire app. It should have this JSDoc type:

    /** @type {import("ruck/serve.mjs").AppComponent} */

    It typically imports and uses several React hooks from Ruck:

    • useCss to declare CSS files that apply to the entire app.
    • useHead to establish head tags that apply to the entire app such as meta[name="viewport"] and link[rel="manifest"].
    • useRoute to get the current route URL and content, and render it in a persistent layout containing global content such as a header and footer.

    Here’s an example public/components/App.mjs module for a website with home and blog pages:

    // @ts-check
    
    import { createElement as h, Fragment, useMemo } from "react";
    import useCss from "ruck/useCss.mjs";
    import useHead from "ruck/useHead.mjs";
    import useRoute from "ruck/useRoute.mjs";
    
    import NavLink, { css as cssNavLink } from "./NavLink.mjs";
    
    const css = new Set([
      ...cssNavLink,
      "/components/App.css",
    ]);
    
    /**
     * React component for the Ruck app.
     * @type {import("ruck/serve.mjs").AppComponent}
     */
    export default function App() {
      const route = useRoute();
    
      useHead(
        // Head tag fragments render in the document head in key order. A good
        // convention is to use group and subgroup numbers, followed by a
        // descriptive name.
        "1-1-meta",
        // Must be memoized. If it’s dynamic use the `useMemo` React hook,
        // otherwise define it outside the component function scope.
        useMemo(() =>
          h(
            Fragment,
            null,
            h("meta", {
              name: "viewport",
              content: "width=device-width, initial-scale=1",
            }),
            h("meta", {
              name: "og:image",
              content:
                // Sometimes an absolute URL is necessary.
                `${route.url.origin}/social-preview.png`,
            }),
            h("link", { rel: "manifest", href: "/manifest.webmanifest" }),
            // More head tags here…
          ), [route.url.origin]),
      );
    
      // This loop doesn’t break React hook rules as the list never changes.
      for (const href of css) useCss(href);
    
      return h(
        Fragment,
        null,
        // Global nav…
        h(
          "nav",
          { className: "App__nav" },
          h(NavLink, { href: "/" }, "Home"),
          h(NavLink, { href: "/blog" }, "Blog"),
        ),
        // Route content…
        route.content,
        // Global footer…
        h("footer", { className: "App__footer" }, "Global footer content."),
      );
    }

    Ruck app route navigation links make use of these React hooks from Ruck:

    • useRoute to get the current route URL path for comparison with the link’s URL path to determine active state.
    • useOnClickRouteLink to replace the default browser navigation that happens when a link is clicked with a Ruck client side route navigation.

    For the previous example, here’s the public/components/NavLink.mjs module:

    // @ts-check
    
    import { createElement as h } from "react";
    import useOnClickRouteLink from "ruck/useOnClickRouteLink.mjs";
    import useRoute from "ruck/useRoute.mjs";
    
    export const css = new Set([
      "/components/NavLink.css",
    ]);
    
    /**
     * React component for a navigation link.
     * @param {object} props Props.
     * @param {string} props.href Link URL.
     * @param {import("react").ReactNode} [props.children] Children.
     */
    export default function NavLink({ href, children }) {
      const route = useRoute();
      const onClick = useOnClickRouteLink();
    
      let className = "NavLink__a";
      if (href === route.url.pathname) className += " NavLink__a--active";
    
      return h("a", { className, href, onClick }, children);
    }

Examples

Requirements

Contributing

Scripts

These CLI scripts are used for development and GitHub Actions CI checks.

Install

To install development dependencies (primarily Puppeteer):

./scripts/install.sh

Test

Beforehand, run the install script. To run the tests:

./scripts/test.sh

Serve

To serve the Ruck project files for testing in other local projects (argument 1 is the localhost port for the HTTP server to listen on):

./scripts/serve.sh 3001

Find min compatible Deno version

To find Ruck’s minimum compatible Deno version:

./scripts/findMinCompatibleDenoVersion.mjs

Type check

To type check every JavaScript module in the project:

./scripts/type-check.mjs

Format

To format the project:

deno fmt

Lint

To lint the project:

deno lint

More Repositories

1

apollo-upload-client

A terminating Apollo Link for Apollo Client that fetches a GraphQL multipart request if the GraphQL variables contain files (by default FileList, File, or Blob instances), or else fetches a regular GraphQL POST or GET request (depending on the config and GraphQL operation).
JavaScript
1,527
star
2

graphql-upload

Middleware and an Upload scalar to add support for GraphQL multipart requests (file uploads via queries and mutations) to various Node.js GraphQL servers.
JavaScript
1,427
star
3

graphql-multipart-request-spec

A spec for GraphQL multipart form requests (file uploads).
994
star
4

graphql-react

A GraphQL client for React using modern context and hooks APIs that is lightweight (< 4 kB) but powerful; the first Relay and Apollo alternative with server side rendering.
JavaScript
718
star
5

apollo-upload-examples

A full stack demo of file uploads via GraphQL mutations using Apollo Server and apollo-upload-client.
JavaScript
427
star
6

Barebones

A barebones boilerplate for getting started on a bespoke front end.
JavaScript
125
star
7

Fix

A CSS normalization/reset reference.
CSS
123
star
8

next-graphql-react

A graphql-react integration for Next.js.
JavaScript
77
star
9

find-unused-exports

A Node.js CLI and equivalent JS API to find unused ECMAScript module exports in a project.
JavaScript
56
star
10

extract-files

A function to recursively extract files and their object paths within a value, replacing them with null in a deep clone without mutating the original value. FileList instances are treated as File instance arrays. Files are typically File and Blob instances.
JavaScript
54
star
11

coverage-node

A simple CLI to run Node.js and report code coverage.
JavaScript
53
star
12

graphql-api-koa

GraphQL execution and error handling middleware written from scratch for Koa.
JavaScript
52
star
13

svg-symbol-viewer

An online, no-upload drag-and-drop SVG file symbol extractor and viewer.
JavaScript
36
star
14

graphql-react-examples

Deno Ruck web app demonstrating graphql-react functionality using various GraphQL APIs.
JavaScript
36
star
15

Purty-Picker

A super lightweight visual HSL, RGB and hex color picker with a responsive, touch-friendly and customizable UI. Compatible with jQuery or Zepto.
JavaScript
36
star
16

fake-tag

A fake template literal tag to trick syntax highlighters, linters and formatters into action.
JavaScript
33
star
17

next-server-context

A Next.js App or page decorator, React context object, and React hook to access Node.js HTTP server context when rendering components.
JavaScript
31
star
18

jsdoc-md

A Node.js CLI and equivalent JS API to analyze source JSDoc and generate documentation under a given heading in a markdown file (such as readme.md).
JavaScript
26
star
19

next-router-events

A more powerful Next.js router events API.
JavaScript
26
star
20

test-director

An ultra lightweight unit test director for Node.js.
JavaScript
25
star
21

device-agnostic-ui

Device agnostic styles, components & hooks for React apps.
JavaScript
18
star
22

snapshot-assertion

Asserts a string matches a snapshot saved in a file. An environment variable can be used to save rather than assert snapshots.
JavaScript
16
star
23

Focal

Simulates a camera focus within a CSS 3D transform powered scene using CSS blur filters.
JavaScript
12
star
24

scroll-animator

Smart, lightweight functions to animate browser scroll.
JavaScript
12
star
25

graphql-http-test

A JavaScript API and CLI to test a GraphQL server for GraphQL over HTTP spec compliance.
JavaScript
12
star
26

eslint-config-env

ESLint config optimized for authoring packages that adapts to the project environment.
JavaScript
11
star
27

nova-deno

A Nova editor extension that integrates the Deno JavaScript/TypeScript runtime and tools.
JavaScript
9
star
28

webpack-watch-server

A single npm script command to start Webpack and your server in watch mode, with a unified console.
JavaScript
9
star
29

Skid

An ultra-lightweight slider utilizing Hurdler for URL hash based control.
JavaScript
8
star
30

Hurdler

Enables hash links to web page content hidden beneath layers of interaction.
JavaScript
7
star
31

audit-age

A Node.js CLI and equivalent JS API to audit the age of installed production npm packages.
JavaScript
7
star
32

ruck-website

The ruck.tech website for Ruck, an open source buildless React web application framework for Deno.
JavaScript
6
star
33

react-waterfall-render

Renders nested React components with asynchronous cached loading; useful for GraphQL clients and server side rendering.
JavaScript
6
star
34

babel-plugin-syntax-highlight

A Babel plugin that transforms the code contents of template literals lead by comments specifying a Prism.js language into syntax highlighted HTML.
JavaScript
6
star
35

google-static-maps-styler-query

Converts a Google Maps styler array to a Google Static Maps styler URL query string.
JavaScript
5
star
36

constraint-validation-buggyfill

Prevents invalid form submission in browsers that improperly support the HTML forms spec (i.e. Safari).
JavaScript
5
star
37

babel-plugin-transform-runtime-file-extensions

A Babel plugin that adds file extensions to Babel runtime import specifiers and require paths for Node.js ESM compatibility.
JavaScript
4
star
38

eslint-plugin-optimal-modules

An ESLint plugin to enforce optimal JavaScript module design.
JavaScript
4
star
39

install-from

Reliably installs a local package into another, for testing.
JavaScript
4
star
40

babel-plugin-transform-require-extensions

A Babel plugin that transforms specified require path file extensions.
JavaScript
3
star
41

device-agnostic-ui-website

The Device Agnostic UI website.
JavaScript
3
star
42

disposable-directory

Asynchronously creates a disposable directory in the OS temporary directory that gets deleted after the callback is done or errors.
JavaScript
3
star
43

class-name-prop

A lightweight utility function to create a React className prop value for multiple class names.
JavaScript
2
star
44

replace-stack-traces

A JavaScript function to replace error stack traces and following Node.js versions at any indent in a multiline string.
JavaScript
2
star
45

revertable-globals

Sets globals in a JavaScript environment that can be easily reverted to restore the original environment; useful for testing code that relies on the presence of certain globals.
JavaScript
2
star
46

eslint-config-barebones

Barebones ESLint config, extending JavaScript Standard Style.
JavaScript
1
star