Archive for the 'General' Category

Using Flow types with Vite, the Hermes way

In my previous post, I outlined how I migrated a Create-React-App application with Flow types to Vite. I used bunchtogether’s lovely little vite-plugin-flow to get the job done.

After some discussion over on the Flowtype Discord I thought it might be worth figuring out whether I could use Hermes‘ parser capabilities instead (as recommended by the Flow team). On the surface it looked like it was going to be really difficult: I’ve always used Hermes as a Babel parser plugin, and Babel doesn’t usually fit into the whole Vite ecosystem particularly well. It turns out to work quite nicely for my use case.

Luckily others had done most of the work for me – I just had to put the pieces together. First up was vite-plugin-babel (from the wonderfully named Owls Department), which enables Babel in Vite’s development mode. Then came Flow’s @babel/preset-flow and babel-plugin-syntax-hermes-parser (which I was already using in the old Create-React-App project):

npm install --save-dev vite-plugin-babel @babel/preset-flow babel-plugin-syntax-hermes-parser

npm uninstall @bunchtogether/vite-plugin-flow

Finally I updated my project’s vite.config.js. I swapped out the imports:

import babel from "vite-plugin-babel";
import { flowPlugin, esbuildFlowPlugin } from "@bunchtogether/vite-plugin-flow";

Then added babel into the plugins section, and removed the vite-plugin-flow config:

{
  plugins: [
    babel({
      babelConfig: {
        babelrc: false,
        configFile: false,
        plugins: ["babel-plugin-syntax-hermes-parser"],
        parserOpts: { flow: "all" },
        presets: ["@babel/preset-flow"],
      },
    }),
    flowPlugin(),
  ],
  optimizeDeps: {
    esbuildOptions: {
      plugins: [esbuildFlowPlugin()],
    },
  },
}

So far everything in my project works nicely, and performance is largely unchanged from the previous setup (using vite-plugin-flow). Production builds on CircleCI come in around 3m30s faster than the previous Create-React-App builds.


The journey to get here took a couple of detours. The most notable was finding the official @vitejs/plugin-react can also do custom Babel plugins/presets. However, it appears support is limited (they do not enable code transformation), and are only applied during the production build. If that had worked, I wouldn’t have needed vite-plugin-babel.

Migrating a Flow typed React SPA project from create-react-app (and Webpack) to Vite

I had a project based on Create React App, using Flow types, which I wanted to migrate to Vite. These are roughly the steps I had to follow:

  1. Most of my JSX files had a .js file extension. I renamed them all to .jsx – Vite is much happier that way
  2. I installed Vite and a few plugins:
    • @bunchtogether/vite-plugin-flow (to allow working with Flow types)
    • @vitejs/plugin-react
    • vite-jsconfig-paths
    • @vitejs/plugin-react-refresh
    • vite-plugin-svgr
yarn install -D vite vite-jsconfig-paths vite-plugin-svgr @vitejs/plugin-react @vitejs/plugin-react-refresh @bunchtogether/vite-plugin-flow
  1. Added a vite.config.js to the project root:
/* eslint-disable flowtype/require-valid-file-annotation */

import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import svgrPlugin from "vite-plugin-svgr";
import { flowPlugin, esbuildFlowPlugin } from "@bunchtogether/vite-plugin-flow";
import jsconfigPaths from "vite-jsconfig-paths";
import * as path from "path";

// https://vitejs.dev/config/
/** @type {import('vite').UserConfig} */
export default defineConfig({
  optimizeDeps: {
    esbuildOptions: {
      plugins: [esbuildFlowPlugin()],
    },
  },
  plugins: [
    flowPlugin(), react(), jsconfigPaths(), svgrPlugin()
  ],
  resolve: {
    alias: [
      { find: "styles", replacement: path.resolve(__dirname, "src/styles") },
    ],
  },
});
  1. Added a new file at /flow-typed/vite_import_meta.js to handle Vite’s handling of environment variables:
// @flow
declare interface Import$Meta extends Import$Meta {
  env: {
    [key: string]: string,
  };
}
  1. Update the paths in my .jsconfig file:
{
  "compilerOptions": {
    "baseUrl": "src",
    "target": "esnext",
    "module": "esnext",
    "moduleResolution": "node",
    "allowSyntheticDefaultImports": true,
    "paths": {
      "flow-types": ["flow-types.js"],
      "polyfills": ["polyfills.js"],
      "apis/": ["apis/"],
      "App/": ["App/"],
      "assets/": ["assets/"],
      "constants/": ["constants/"],
      "errors/": ["errors/"],
      "hoc/": ["hoc/"],
      "hooks/": ["hooks/"],
      "redux/": ["redux/"],
      "styles/": ["styles/"],
      "utils/": ["utils/"],
      "validators/": ["validators/"]
    }
  },
  "include": ["src"],
  "exclude": [
    "node_modules",
    ".storybook",
    "Auth0",
    "flow-typed",
    "dist",
    ".idea"
  ]
}
  1. Change the prefix on all .env variables from REACT_APP_ to VITE_
  2. [ ] Change all instances of process.env["REACT_APP_WHATEVER"] to import.meta.env.VITE_WHATEVER
  3. Move index.html from public to the project root, and update any URLs to use %VITE_WHATEVER%
  4. Add this to the ignore section of your .flowconfig:
    /vite.config.js
  5. I ran into issues with circular references. I was able to find them with: npx madge src/index.jsx --circular
  6. Test like hell!

Thanks to Cathal Mac Donnacha, whose post about migrating a typescript project from CRA to Vite was hugely helpful.

Update: I’ve since migrated the project to use Flow’s Hermes parser.

UROC Secures Ongoing Land Access Agreements

United Riders of Cumberland (“UROC”) has reached a landmark land access and management framework in order for the community and visitors to continue to enjoy access to the trails on the private and municipal lands surrounding Cumberland, BC.

https://unitedridersofcumberland.com/uroc-secures-ongoing-land-access-agreements/

A nice bit of news to brighten up a wet and rainy Tuesday morning.

Overcast is discontinuing the website player

I’ve decided to discontinue most of the web player’s functionality in 2023 and focus exclusively on the Overcast app for iPhone, iPad, Apple Watch, and M1/M2-powered Macs.

Marco Arment – https://overcast.fm/webplayerdiscontinuation

Sigh. I understand Marco’s rationale, but it’s still disappointing – I use the website player quite a lot.

Update: It’s not going away after all! 🙂

When a Transition component never reaches the exited state in React 18

After upgrading to React 18, I ran into an issue where a Transition component would never reach the exited state. Specifically it happens when navigating to a new route whose components were loaded using Suspense and React.lazy.

As is typical, I found someone else facing a very similar issue, but no workaround or fix was posted.

For me the answer was to give the Transition component a unique key prop, e.g.

const Example = () => {
  // Pull a key from Reach Router
  const { key } = useLocation();

  return (
    <Transition
      key={key} 
      // etc...

Et voila! The transitions work properly again.

Event handling with react-signature-pad-wrapper

I have a React app which makes use of react-signature-pad-wrapper (which is a React wrapper for signature_pad). My component needs to call a function whenever someone stops drawing on the canvas. Until recently, I could use the onEnd event like so:

const signaturePadOptions = {
  minWidth: 1,
  maxWidth: 5,
  penColor: "rgb(0, 0, 0)",
  // This part doesn't work anymore:
  onEnd: handleEndStroke, 
}

Whenever the onEnd event was fired, my handleEndStroke function was called. But that changed in version 4 of signature-pad: Now we have to listen for endStroke events on the signaturePad. I’ve used the useEffect hook with addEventListener in my React component:

useEffect(() => {
  const handleEndStroke = () => {
    // I do my custom stuff here
  };

  if (!!ref.current && !!ref.current.signaturePad) {
    const current = ref.current;
    // initiate the event handler
    current.signaturePad.addEventListener(
      "endStroke",
      handleEndStroke,
      false
    );
    // clean up the event handler
    return function cleanup() {
      current.signaturePad.removeEventListener(
        "endStroke",
        handleEndStroke
      );
    };
  }
}, [ref]);

Setting custom HTTP headers on a Cloudflare Pages site

I have a React app (built with create-react-app) which I’m hosting on Cloudflare pages. I wanted to add a X-Robots-Tag HTTP header to every page on the site. It turns out to be really quite easy:

Create a new file called _headers in your public folder, and put this inside:

/*
  X-Robots-Tag: noindex, nofollow  

Obviously this is a really simple use-case, but there’s a lot more you can do. Headers take the following format:

[url]
  [name]: [value]

You can have multiple name/value pairs under a given URL. URLs can contain placeholders and wildcards (called “splats”) to help widen or narrow down where they apply. So to stop anything in the /app folder from being shown in an iframe you might do:

/app/*
  X-Frame-Options: DENY

You can set multiple headers for the same URL, e.g.

/app/*
  X-Frame-Options: SAMEORIGIN
  Referrer-Policy: same-origin
  X-Content-Type-Options: nosniff

Lets say your production site is on a custom domain, and the dev/test versions live on Cloudflare’s pages.dev domain. You don’t want search engines to index your dev/test versions, so you can send the X-Robots-Tag header only on those domains, using the placeholder functionality:

# Swap projectname below for your own project's name
https://:projectname.pages.dev/*
  X-Robots-Tag: noindex, nofollow  

There’s more examples over in their documentation.

Initial thoughts on Cloudflare Pages

I’m investigating Cloudflare Pages as a hosting platform for static sites and React SPAs (amongst other things).

My first impression: This is a really simple tool which handles almost everything about hosting for me. I’m practically sold already. Hook it up to your Github (or Gitlab) repository and for the most part, everything else happens automagically. That simplicity does come with limitations though, so it might not suit everybody.

Custom domains

Hooking up a custom domain to your CF Pages site is really simple. It’s completely automated if Cloudflare manages your DNS. If not, it’s just a case of adding a CNAME entry wherever your DNS is managed. So if I wanted to point sub.example.com to my CF pages site at example.pages.dev, i’d deploy this CNAME record:

NAMETYPEVALUE
sub.example.comCNAMEexample.pages.dev

I was also able to point a custom domain at the latest deployment from a specific Git branch, in this case the develop branch:

NAMETYPEVALUE
sub-develop.example.comCNAMEdevelop.example.pages.dev

Environments and Previews

CF Pages can be a bit limiting if you have multiple deployment environments (e.g. development, staging, production). You can only have two sets of environment variables – one for Production and another for everything else. If you need more than that, you might want to look elsewhere for now. Luckily, it’s enough for me (for now).

Speaking of environment variables, at present you have to set them using their web interface, instead of a config file. I don’t have many, so it’s not an issue for me – but if your app makes heavy use of them, they might become a bit cumbersome to manage.

With all that said, CF Pages generates a preview build for every commit you push to Github. This is useful for getting someone else to test your work before merging it, and may reduce the need for different environments. Even if you don’t use CF Pages as your main hosting platform, the preview builds are a useful way to test your site before you go to production.

Workers and server-side code

I haven’t gotten into it yet, but CF Pages now integrates with CF Workers, which is their variant on Cloud Functions / Lambda / Serverless. You also get access to KV, their key-value data store. This means Cloudflare Pages doesn’t just need to be for static sites – there’s potentially a lot more flexibility available.

Other limitations and known issues

Handily, Cloudflare have documented some of the known issues and limitations of CF Pages.