SaltyCrane Blog — Notes on JavaScript and web development

My Claude Code tips and links

Note: I wrote these tips before reading the Claude Code Best practices document. It is good. Maybe read that first.

I started using Claude Code ~July 2026 when the company switched from Copilot to Claude. Here are some of my tips. Please share yours as well. These are more on the end of the "use AI to help you code" spectrum, as opposed to "loop, graph, and let AI do all the things", in part due to the company having a limited number of tokens to use. I mostly stopped using subagents when Copilot switched to usage-based billing in June 2026 since they are no longer free and I have to preserve my precious tokens. Also these notes come from the context of a frontend developer.

Claude Code tips

TODO

  • Use claude --bare. This looks like a good step to make Claude Code closer to Pi. I tried running claude --bare and /context showed 0 tokens used, which I didn't know what possible with Claude Code. Normally I am starting at 11.3k tokens in a fresh session.

Questions

  • ast-grep. Pi + gpt-5.6-sol happily used ast-grep with this ast-grep skill I found a while ago, but Claude Code doesn't like to use it. Anyone have success using it with Claude Code?

Other links

Magit, fzf, and ast-grep demo

This is a demo I did at work on Magit, fzf, and ast-grep. I used Magit since 2016, fzf since 2018, and ast-grep first in 2024. The Magit usage is on git interactive rebase, so it might be translated to other git clients. I recently switched from fzf to atuin, but fzf is still great. The fuzzy matching with fzf seemed to work better for me, but I think the extra metadata saved with atuin might be useful. At the time of the demo, I was not able to get the "replace" working with ast-grep. I created a demo of my issue. It's possible the issue could have been fixed since there have been many new releases since then. Or I might have been doing something wrong. I haven't used ast-grep in a while, but I did see an AI agent that advertised ast-grep integration, which sounds cool. The HN comments do say that command line tools are on the rise integrating with AI agents, so let's CLI.

The video on YouTube is here: https://www.youtube.com/watch?v=W4eOz3L6Ga8. Sorry for the poor resolution. I don't know how to YouTube.

How to use ast-grep with GraphQL

ast-grep is a code search + replace tool that uses the abstract syntax tree (AST). It has language support for TypeScript, Python, and others, but it doesn't support GraphQL out of the box. However, there is documentation for supporting other languages: Custom Language Support.

These are my notes using that Custom Language Support documentation along with the Multi-Language Documents documentation to set up GraphQL support for ast-grep on macOS.

Prepare Tree-sitter Tool and Parser

  • Install tree-sitter CLI

    brew install tree-sitter
    

    (Alternatively, npm install -g tree-sitter-cli)

  • Test tree-sitter is installed

    tree-sitter --version
    
    tree-sitter 0.23.0
    
  • Use a working directory

    I'll use /tmp as my working directory, but a different directory can be used

    cd /tmp
    
  • Get the tree-sitter GraphQL grammar

    git clone https://github.com/bkegley/tree-sitter-graphql.git
    

    (This is the repo used by the neovim tree-sitter library.)

Compile the Parser as a Dynamic Library

  • Compile the GraphQL parser as a dynamic library

    cd tree-sitter-graphql
    export TREE_SITTER_LIBDIR=/tmp
    tree-sitter test
    

    Note: I got the following error when I ran this. If anyone knows what is going on, please let me know.

    Error in query file "formatter.scm"
    
    Caused by:
        Query error at 5:31. Impossible pattern:
        (input_object_type_definition (input_value_definition) @field_definition)
    

    However, it still generated the graphql.dylib file in /tmp. Note: according to ChatGPT, .dylib is used on Mac, .so is used on Linux, and .dll is used on Windows.

Register Language in sgconfig.yml

  • Create a new ast-grep project

    cd /tmp
    sg new
    
    No sgconfig.yml found. Creating a new ast-grep project...
    > Where do you want to have your rules? rules
    > Do you want to create rule tests? No
    > Do you want to create folder for utility rules? No
    Your new ast-grep project has been created!
    
  • Edit sgconfig.yml to add GraphQL as a custom language

    /tmp/sgconfig.yml:

    ruleDirs:
    - ./rules
    customLanguages:
    graphql:
        libraryPath: graphql.dylib
        extensions: [graphql]
        expandoChar: $
    

Use It!

  • Search for all input types in /path/to/my/schema.graphql

    sg -p "input" -l graphql /path/to/my/schema.graphql
    
    /path/to/my/schema.graphql
    6│input AddOrUpdateResidualWorksheetInputType {
    120│input AutoAssignStyleCodeInputType {
    478│input BulkCreateVehicleReleaseInputType {
    483│input BulkDeleteVehicleReleaseInputType {
    487│input BulkEditVehicleReleaseInputType {
    492│input BulkImportStyleFromCppInputType {
    949│input CloneModelInputType {
    958│input CloneModelStyleInputType {
    973│input CloneStyleInputType {
    1749│input CppConfigurationInputType {
    
  • Search for all DateTime input fields (but not output fields) in /path/to/my/schema.graphql. Note: the tree-sitter-graphql grammer.js file contains different kinds that can be used for filtering.

    sg scan --inline-rules '
    id: datetimeinputs
    language: graphql
    rule:
      regex: "DateTime"
      kind: input_value_definition
    ' /path/to/my/schema.graphql
    
    help[datetimeinputs]:
         ┌─ /path/to/my/schema.graphql:2356:3
         │
    2356 │   configurationLastUpdatedAt: DateTime
         │   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    
    help[datetimeinputs]:
         ┌─ /path/to/my/schema.graphql:8331:69
         │
    8331 │   previousStyleSnapshot(styleSnapshotRowId: Guid, styleRowId: Long, versionDate: DateTime): StyleSnapshotTypeOrOperationErrorType
         │                                                                     ^^^^^^^^^^^^^^^^^^^^^
    
    help[datetimeinputs]:
         ┌─ /path/to/my/schema.graphql:8794:16
         │
    8794 │   whatsChanged(date: DateTime!, flags: [WhatsChangedFilterFlagsEnumType], includeCppOrEmbargoedData: CppEmbargoedDataFilterEnumType): WhatsChangedType
         │                ^^^^^^^^^^^^^^^
    
    help[datetimeinputs]:
          ┌─ /path/to/my/schema.graphql:11536:3
          │
    11536 │   configurationLastUpdatedAt: DateTime
          │   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    

Next.js App Router (RSC) projects w/ open source code

Here are some example projects with open source code using Next.js App Router and React Server Components (RSC).

2024-10-24 additions

(Tiling) window managers for macOS

More hacker-oriented

More usable out-of-the-box

Related: focus follows mouse for macOS

See also

Next.js Relay GraphQL Pokemon example

Here is a quick and dirty Pokemon TCG web UI using Next.js, Relay, and the TCGdex GraphQL API. Initially this was supposed to be a proof-of-concept of the Next.js rewrites feature, but that doesn't work with the Next.js static export for the GitHub Pages deploy, so I removed it.

Source code on GitHub here: https://github.com/saltycrane/next-relay-graphql-pokemon-example

Deployed to GitHub Pages here: https://saltycrane.github.io/next-relay-graphql-pokemon-example/

Uses

Doesn't use

  • Next.js App Router or React Server Components
  • Next.js Server Side Rendering
  • Next.js Image Optimization
  • React Transitions
  • Relay Fragments

Example Node.js Passport.js SAML app using OneLogin

I put an example Express.js, Passport.js OneLogin SAML SSO authentication app on github here: https://github.com/saltycrane/express-passport-saml-example. I used the following:

OneLogin configuration

  • create OneLogin developer account here: https://developers.onelogin.com/
  • for example, use the domain your-domain
  • at https://your-domain-dev.onelogin.com/admin2/apps select "Add App" > "SAML Custom Connector (Advanced)"
  • on "Configuration" tab, set the following 5 fields:
    • "Audience (EntityID)" [1]: your-example-app
    • "Recipient": your-example-app
    • "ACS (Consumer) URL Validator*": http://localhost:3000/login/sso/callback
    • "ACS (Consumer) URL*": http://localhost:3000/login/sso/callback
    • "SAML signature event" [1]: "Both"

[1] required as of node-saml v4.0.0

Set environment variables

  • copy .env.example to .env and change the following:
    • SSO_ENTRYPOINT: "SSO" tab > "SAML 2.0 Endpoint (HTTP)"
    • SSO_CERT: "SSO" tab > "X.509 Certificate" > "View Details" > "X.509 Certificate" with "-----BEGIN CERTIFICATE-----" and "-----END CERTIFICATE-----" and newlines removed
    • SSO_COOKIE_SESSION_SECRET: generate or make up a secret string

Note: SSO_ISSUER should be "Recipient" on the "Configuration" tab and SSO_CALLBACK_URL should be "ACS (Consumer) URL*" on the "Configuration" tab.

Example .env

SSO_ENTRYPOINT='https://your-domain-dev.onelogin.com/trust/saml2/http-post/sso/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'

SSO_ISSUER='your-example-app'

SSO_CALLBACK_URL='http://localhost:3000/login/sso/callback'

SSO_CERT='XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX='

SSO_COOKIE_SESSION_SECRET='xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'

Run app and test

Sequence of requests

  1. GET http://localhost:3000/login/sso
  2. GET https://your-domain-dev.onelogin.com/trust/saml2/http-post/sso/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
  3. POST http://localhost:3000/login/sso/callback
  4. GET http://localhost:3000/ (with user session cookie)

Troubleshooting

"Access Denied You do not have access to this application. Please contact your administrator."
  • ensure your user is added to the default role for the app in the OneLogin admin UI.
Error: Invalid signature
  • in the OneLogin admin UI, in the "Configuration" tab, ensure that "SAML signature element" is set to "Both"
  • alternatively, as a less secure option, add the following configuration to the passport-saml Strategy: wantAssertionsSigned: false.
  • node-saml changed in v4.0.0 to require all assertions be signed. See https://github.com/node-saml/node-saml/pull/177
Error: SAML assertion AudienceRestriction has no Audience value
  • in node-saml, audience defaults to the value of issuer
  • in the OneLogin admin UI, in the "Configuration" tab, ensure that "Audience (EntityID)" is the same as issuer. (In this example it is the value of "Recipient", your-example-app)

CSS Subgrid demo

This is a demo I did on CSS Subgrid.

It is a Next.js React project using CSS Modules. (As mentioned in the video, I wanted to use vanilla HTML and CSS for the demo, but this was just easier for me since I'm familiar with it.)

The code in the repo is slightly different than the code in the YouTube demo because I converted from using PostgreSQL to SQLite so I could include the data in the repo.

Aphrodite to CSS Modules codemod

I wanted to convert our React project from Aphrodite to CSS Modules. The biggest impetus was that Aphrodite isn't supported by the new Next.js v13 app directory feature, which I'm excited to try. I like styled-components, but my co-worker likes CSS Modules and it's hard to go wrong with CSS Modules. CSS Modules also has built-in support in Next.js and it looks pretty good in this graphic from the State of CSS survey.

To ease the conversion, I wrote a jscodeshift codemod to automate most of the process. The codemod is on github here: aphrodite-to-css-modules-codemod. An example is below.

The codemod worked well for my 200 Aphrodite files. I did spend time manually converting JS constants into CSS variables. I also manually handled CSS precedence issues since Aphrodite handles precedence more nicely than CSS. But overall I was pretty happy with the results. (It was certainly more successful than my attempt at a reactstrap-to-react-bootstrap codemod which I never used.)

Before

./example/src/MyComponent.tsx:

import { css, StyleSheet } from "aphrodite";
import classNames from "classnames";
import React from "react";

import { colors } from "./constants";
import { hexToRgbA } from "./utils";

export default function MyComponent() {
  const isSomething = true;
  const isSomethingElse = false;
  return (
    <div
      className={css(
        isSomethingElse ? myStyles.containerGrid : myStyles.containerFlex,
      )}
      style={{}}
    >
      <div className={css(myStyles.header, myStyles.content)}>header</div>
      <div className={classNames(css(myStyles.content), "another-class")}>
        <div>Lorem ipsum</div>
      </div>
      <span className={css(isSomething && myStyles.warning)}></span>
    </div>
  );
}

// comment I
export const myStyles = StyleSheet.create({
  containerGrid: {
    backgroundColor: "white",
    // comment 1
    /* comment 2 */ display: "grid" /* comment 4 */, // comment 5
    gridTemplate: `
      "sourceselect .       reviewbutton" auto
      "pagination   filters filters     " auto
      "rowcount     filters filters     " 20px
      / 2fr         1fr     2fr
    `,
    width: 200,
  },
  containerFlex: {
    display: "flex",
  },
  content: {
    lineHeight: 1.5,
  },
  header: {
    backgroundColor: "#ccc",
    color: hexToRgbA(colors.danger, 0.8),
    display: "inline-block",
    ":hover": {
      color: colors.primary,
      borderColor: `${colors.info} !important`,
    },
  },
  // comment a
  warning: {
    fontWeight: 700,
    color: colors.warning,
    opacity: 0,
  } /* comment b */, // comment c
});

After

./example/src/MyComponent.tsx:

import myStyles from "./MyComponent.module.css";
import classNames from "classnames";
import React from "react";

import { colors } from "./constants";
import { hexToRgbA } from "./utils";

export default function MyComponent() {
  const isSomething = true;
  const isSomethingElse = false;
  return (
    <div
      className={
        isSomethingElse ? myStyles.containerGrid : myStyles.containerFlex
      }
      style={{}}
    >
      <div
        className={
          // TODO: check CSS precedence
          classNames(myStyles.header, myStyles.content)
        }
      >
        header
      </div>
      <div className={classNames(myStyles.content, "another-class")}>
        <div>Lorem ipsum</div>
      </div>
      <span className={classNames(isSomething && myStyles.warning)}></span>
    </div>
  );
}

export { myStyles };

./example/src/MyComponent.module.css:

/* comment I */
.containerGrid {
  background-color: white;
  /* comment 1 */
  /* comment 2 */
  display: grid; /* comment 4 */ /* comment 5 */
  grid-template: 
  "sourceselect .       reviewbutton" auto
  "pagination   filters filters     " auto
  "rowcount     filters filters     " 20px
  / 2fr         1fr     2fr
;
  width: 200px;
}

.containerFlex {
  display: flex;
}

.content {
  line-height: 1.5;
}

.header {
  background-color: #ccc;
  color: var(--bs-danger-alpha80);
  display: inline-block;
}

.header:hover {
  color: var(--bs-primary);
  border-color: var(--bs-info) !important;
}

/* comment a */
.warning {
  font-weight: 700;
  color: var(--bs-warning);
  opacity: 0;
} /* comment b */ /* comment c */

JS context file

The expressions in the styles object (e.g. colors.danger, hexToRgbA(colors.danger, 0.8), etc.) were evaluated using the following "context" file.

./context.example.js:

const colors = {
  danger: "var(--bs-danger)",
  info: "var(--bs-info)",
  primary: "var(--bs-primary)",
  warning: "var(--bs-warning)",
};

function hexToRgbA(hex, alpha) {
  return hex.replace(/\)$/, `-alpha${alpha * 100})`);
}

Simple codemod example with jscodeshift

jscodeshift codemods allow refactoring JavaScript or TypeScript code by manipulating the abstract syntax tree.

This is an example showing how to rename variables named foo to bar.

Install jscodeshift

npm install -g jscodeshift

Create an example file to modify

  • create a folder

    mkdir my-project
    cd my-project
    
  • create an example file, my-file-to-modify.js

    const foo = 1;
    console.log(foo);
    

Create a transform

create a file my-transform.js

module.exports = function transformer(fileInfo, api) {
  return api
    .jscodeshift(fileInfo.source)
    .find(api.jscodeshift.Identifier)
    .forEach(function (path) {
      if (path.value.name === "foo") {
        api.jscodeshift(path).replaceWith(api.jscodeshift.identifier("bar"));
      }
    })
    .toSource();
};

Run it

jscodeshift -t my-transform.js ./my-file-to-modify.js

The file my-file-to-modify.js now contains:

const bar = 1;
console.log(bar);

Another example

This example removes the React JSX element <MyHeader /> and removes the MyHeader import. I'm not sure why, but it added some extra parentheses. Prettier cleaned this up for me, but if you have an improvement, let me know.

// removeMyHeader.js
module.exports = function transformer(file, api) {
  const jscodeshift = api.jscodeshift;

  const withoutElement = jscodeshift(file.source)
    .find(jscodeshift.JSXElement)
    .forEach(function (path) {
      if (path.value.openingElement.name.name === "MyHeader") {
        path.prune();
      }
    })
    .toSource();

  const withoutImport = jscodeshift(withoutElement)
    .find(jscodeshift.ImportDefaultSpecifier)
    .forEach(function (path) {
      if (path.value.local.name === "MyHeader") {
        path.parentPath.parentPath.prune();
      }
    })
    .toSource();

  return withoutImport;
};

Here is a command to run it for a React TypeScript codebase:

jscodeshift --parser=tsx --extensions=tsx -t ./removeMyHeader.js ./src

AST Explorer

AST Explorer is a very helpful tool to experiment and learn the API with code completion. Go to https://astexplorer.net/ and select "jscodeshift" under the "Transform" menu.

lodash error

Error: Cannot find module 'lodash'

When running jscodeshift, I got the above error so I ran npm install -g lodash and this got rid of the error for me.