A blog and website by Peter Bengtsson
At the time of writing, I don't know if this is the optimal way, but after some trial and error, I got it working.
This example demonstrates a hook that gives you the current value of the ?view=...
(or a default) and a function you can call to change it so that ?view=before
becomes ?view=after
.
In NextJS v13 with the pages
directory:
import { useRouter } from "next/router";
export function useNamesView() {
const KEY = "view";
const DEFAULT_NAMES_VIEW = "buttons";
const router = useRouter();
let namesView: Options = DEFAULT_NAMES_VIEW;
const raw = router.query[KEY];
const value = Array.isArray(raw) ? raw[0] : raw;
if (value === "buttons" || value === "table") {
namesView = value;
}
function setNamesView(value: Options) {
const [asPathRoot, asPathQuery = ""] = router.asPath.split("?");
const params = new URLSearchParams(asPathQuery);
params.set(KEY, value);
const asPath = `${asPathRoot}?${params.toString()}`;
router.replace(asPath, asPath, { shallow: true });
}
return { namesView, setNamesView };
}
In NextJS v13 with the app
directory.
import { useRouter, useSearchParams, usePathname } from "next/navigation";
type Options = "buttons" | "table";
export function useNamesView() {
const KEY = "view";
const DEFAULT_NAMES_VIEW = "buttons";
const router = useRouter();
const searchParams = useSearchParams();
const pathname = usePathname();
let namesView: Options = DEFAULT_NAMES_VIEW;
const value = searchParams.get(KEY);
if (value === "buttons" || value === "table") {
namesView = value;
}
function setNamesView(value: Options) {
const params = new URLSearchParams(searchParams);
params.set(KEY, value);
router.replace(`${pathname}?${params}`);
}
return { namesView, setNamesView };
}
The trick is that you only want to change 1 query string value and respect whatever was there before. So if the existing URL was /page?foo=bar
and you want that to become /page?foo=bar&and=also
you have to consume the existing query string and you do that with:
const searchParams = useSearchParams();
...
const params = new URLSearchParams(searchParams);
params.set('and', 'also')
Cheerio is a fantastic Node library for parsing HTML and then being able to manipulate and serialize it. But you can also just use it for parsing HTML and plucking out what you need. We use that to prepare the text that goes into our search index for our site. It basically works like this:
const body = await getBody('http://localhost:4002' + eachPage.path)
const $ = cheerio.load(body)
const title = $('h1').text()
const intro = $('p.intro').text()
...
But it hit me, can we speed that up? cheerio
actually ships with two different parsers:
One is faster and one is more strict.
But I wanted to see this in a real-world example.
So I made two runs where I used:
const $ = cheerio.load(body)
in one run, and:
const $ = cheerio.load(body, { xmlMode: true })
in another.
After having parsed 1,635 pages of HTML of various sizes the results are:
FILE: load.txt MEAN: 13.19457640586797 MEDIAN: 10.5975 FILE: load-xmlmode.txt MEAN: 3.9020372860635697 MEDIAN: 3.1020000000000003
So, using {xmlMode:true}
leads to roughly a 3x speedup.
I think it pretty much confirms the original benchmark, but now I know based on a real application.
If you've used GitHub Actions before you might be familiar with the matrix
strategy. For example:
name: My workflow
jobs:
build:
strategy:
matrix:
version: [10, 12, 14, 16, 18]
steps:
- name: Set up Node ${{ matrix.node }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node }}
...
But what if you want that list of things in the matrix to be variable? For example, on rainy days you want it to be [10, 12, 14]
and on sunny days you want it to be [14, 16, 18]
. Or, more seriously, what if you want it to depend on how the workflow is started?
You can make a workflow run on a schedule, on pull requests, on pushes, on manual "Run workflow", or as a result on some other workflow finishing.
First, let's set up some sample on
directives:
name: My workflow
on:
workflow_dispatch:
schedule:
- cron: '*/5 * * * *'
workflow_run:
workflows: ['Build and Deploy stuff']
types:
- completed
The workflow_dispatch
makes it so that a button like this appears:
The schedule
, in this example, means "At every 5th minute"
And workflow_run
, in this example, means that it waits for another workflow, in the same repo, with name: 'Build and Deploy stuff'
has finished (but not necessarily successfully)
For the sake of the demo, let's say this is the rule:
[16, 18]
. [18]
. Build and Deploy stuff
workflow has successfully finished, you want the matrix to be [10, 12, 14, 16, 18]
.It's arbitrary but it could be a lot more complex than this.
What's also important to appreciate is that you could use individual steps that look something like this:
- steps:
- name: Only if started on a workflow_dispatch
if: ${{ github.event_name == 'workflow_dispatch' }}
run: echo "yes it was run because of a workflow_dispatch"
But the rest of the workflow is realistically a lot more complex with many steps and you don't want to have to sprinkle the line if: ${{ github.event_name == 'workflow_dispatch' }}
into every single step.
The solution to avoiding repetition is to use a job that depends on another job. We'll have a job that figures out the array for the matrix
and another job that uses that.
First we inject a job that looks like this:
jobs:
matrix_maker:
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.set-matrix.outputs.result }}
steps:
- uses: actions/github-script@v6
id: set-matrix
with:
script: |
if (context.eventName === "workflow_dispatch") {
return [18]
}
if (context.eventName === "schedule") {
return [16, 18]
}
if (context.eventName === "workflow_run") {
if (context.payload.workflow_run.conclusion === "success") {
return [10, 12, 14, 16, 18]
}
throw new Error(`It was a workflow_run but not success ('${context.payload.workflow_run.conclusion}')`)
}
throw new Error("Unable to find a reason")
- name: Debug output
run: echo "${{ steps.set-matrix.outputs.result }}"
Now we can write the "meat" of the workflow that uses this output:
build:
needs: matrix_maker
strategy:
matrix:
version: ${{ fromJSON(needs.matrix_maker.outputs.matrix) }}
steps:
- name: Set up Node ${{ matrix.version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.version }}
Combined, the entire thing can look like this:
name: My workflow
on:
workflow_dispatch:
schedule:
- cron: '*/5 * * * *'
workflow_run:
workflows: ['Build and Deploy stuff']
types:
- completed
jobs:
matrix_maker:
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.set-matrix.outputs.result }}
steps:
- uses: actions/github-script@v6
id: set-matrix
with:
script: |
if (context.eventName === "workflow_dispatch") {
return [18]
}
if (context.eventName === "schedule") {
return [16, 18]
}
if (context.eventName === "workflow_run") {
if (context.payload.workflow_run.conclusion === "success") {
return [10, 12, 14, 16, 18]
}
throw new Error(`It was a workflow_run but not success ('${context.payload.workflow_run.conclusion}')`)
}
throw new Error("Unable to find a reason")
- name: Debug output
run: echo "${{ steps.set-matrix.outputs.result }}"
build:
needs: matrix_maker
strategy:
matrix:
version: ${{ fromJSON(needs.matrix_maker.outputs.matrix) }}
steps:
- name: Set up Node ${{ matrix.version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.version }}
I've extrapolated this demo from a more complex one at work. (this is my defense for typos and why it might fail if you verbatim copy-n-paste this). The bare bones are there for you to build on.
In this demo, I've used actions/github-script
with JavaScript, because it's convenient and you don't need do to things like actions/checkout
and npm ci
if you want this to be a standalone Node script. Hopefully you can see that this is just a start and the sky's the limit.
Thanks to fellow GitHub Hubber @joshmgross for the tips and help!
Also, check out Tips and tricks to make you a GitHub Actions power-user
Rome is a new contender to compete with Prettier and eslint, combined. It's fast and its suggestions are much easier to understand.
I have a project that uses .js
, .ts
, and .tsx
files. At first, I thought, I'd just use rome
to do formatting but the linter part was feeling nice as I was experimenting so I thought I'd kill two birds with one stone.
My little project only has 28 files, but time rome check lib scripts components *.ts
consistently takes 0.08 seconds.
You get this nice prompt after running npx rome init
the first time:
Easy to understand and needs no explanation because the suggested fix tells a story that means it's immediately easy to understand what the warning is trying to say.
If I run npx create-next-app@latest
, say yes to Eslint, and then run npm I -D prettier
, the node_modules
becomes 275.3 MiB.
Whereas if I run npx create-next-app@latest
, say no to Eslint, and then run npm I -D rome
, the node_modules
becomes 200.4 MiB.
rome.json
's JSON schema works in VS CodeI don't know how this magically worked, but I'm guessing it just does when you install the Rome VS Code extension. Neat with autocomplete!
Almost all things that I'm going to "complain" about is down to usability. I might look back at this in a year (or tomorrow!) and laugh at myself for being dim, but it nevertheless was part of my experience so it's worth pointing out.
It's confusing what is what. If lint
means checking without modifying, what is check
then? I'm guessing rome format
means run the lint
but with permission to edit my files.
What is rome format
compared to rome check --apply
then??
I guess rome check --apply
doesn't just complain but actually applies the things it spots. So what is rome check --apply-suggested
?? (if you're reading this and feel eager to educate me with a comment, please do, but I'm trying to point out that it's not user-friendly)
Unfortunately, in this project, not all files are in one single directory (e.g. rome check src/
is not an option). How do I specify a wildcard expression?
▶ rome check *.ts
Checked 3 files in 942µs
Cool, but how do I do all .ts
files throughout the project?
▶ rome check "**/*.ts"
**/*.ts internalError/io ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✖ No such file or directory (os error 2)
Checked 0 files in 66µs
Clearly, it's not this:
▶ rome check **/*.ts
...
The number of diagnostics exceeds the number allowed by Rome.
Diagnostics not shown: 1018.
Checked 2534 files in 1387ms
Skipped 1 files
Error: errors where emitted while running checks
...because bash will include all the files from node_modules/**/*.ts
.
In the end, I ended up with this (in my package.json
):
"scripts": { "code:lint": "rome check lib scripts components *.ts", ...
Yes, I can contribute this back to the documentation, but today's not the day to do that.
It took me a long time to find out how to disable certain rules (in the rome.json
file) and finally I landed on this:
{ "linter": { "enabled": true, "rules": { "recommended": true, "style": { "recommended": true, "noImplicitBoolean": "off" }, "a11y": { "useKeyWithClickEvents": "off", "useValidAnchor": "warn" } } } }
Much better than having to write inline code comments with the source files themselves.
However, it's still not clear to me what "recommended": true
means. Is it shorthand for listing all the default rules all set to true
? If I remove that, are no rules activated?
rome.json
file is JSONJSON is cool for many things, but writing comments is not one of them.
For example, I don't know what would be better, Yaml or Toml, but it would be nice to write something like:
"a11y": { # Disabled because of issue #1234 # Consider putting this back in December after the refactor launch "useKeyWithClickEvents": "off",
When create-react-app
first came onto the scene, the coolest thing was the zero-config webpack
. But, if you remember, it also came with a really nice zero-config eslint
configuration for React apps. It would even print warnings when the dev server was running. Now it's many years later and good linting config is something you depend/rely on in a framework. Like it or not, there are specific things in Nextjs that is exclusive to that framework. It's obviously not an easy people-problem to solve but it would be nice if Nextjs and rome
could be best friends so you get all the good linting ideas from the code Nextjs framework but all done using rome
instead.
tl;dr sort myfile.log | uniq -c | sort -n -r
I wanted to count recurring lines in a log file and started writing a complicated Python script but then wondered if I can just do it with bash basics.
And after some poking and experimenting I found a really simple one-liner that I'm going to try to remember for next time:
You can't argue with the nice results :)
▶ cat myfile.log
one
two
three
one
two
one
once
one
▶ sort myfile.log | uniq -c | sort -n -r
4 one
2 two
1 three
1 once
tl;dr; fd -I -t d node_modules | rg -v 'node_modules/(\w|@)' | xargs du -sh | sort -hr
It's very possible that there's a tool that does this, but if so please enlighten me.
The objective is to find which of all your various projects' node_modules
directory is eating up the most disk space.
The challenge is that often you have nested node_modules
within and they shouldn't be included.
The command uses fd
which comes from brew install fd
and it's a fast alternative to the built-in find
. Definitely worth investing in if you like to live fast on the command line.
The other important command here is rg
which comes from brew install ripgrep
and is a fast alternative to built-in grep
. Sure, I think one can use find
and grep
but that can be left as an exercise to the reader.
▶ fd -I -t d node_modules | rg -v 'node_modules/(\w|@)' | xargs du -sh | sort -hr 1.1G ./GROCER/groce/node_modules/ 1.0G ./SHOULDWATCH/youshouldwatch/node_modules/ 826M ./PETERBECOM/django-peterbecom/adminui/node_modules/ 679M ./JAVASCRIPT/wmr/node_modules/ 546M ./WORKON/workon-fire/node_modules/ 539M ./PETERBECOM/chiveproxy/node_modules/ 506M ./JAVASCRIPT/minimalcss-website/node_modules/ 491M ./WORKON/workon/node_modules/ 457M ./JAVASCRIPT/battleshits/node_modules/ 445M ./GITHUB/DOCS/docs-internal/node_modules/ 431M ./GITHUB/DOCS/docs/node_modules/ 418M ./PETERBECOM/preact-cli-peterbecom/node_modules/ 418M ./PETERBECOM/django-peterbecom/adminui0/node_modules/ 399M ./GITHUB/THEHUB/thehub/node_modules/ ...
How it works:
fd -I -t d node_modules
: Find all directories called node_modules
but ignore any .gitignore
directives in their parent directories.rg -v 'node_modules/(\w|@)'
: Exclude all finds where the word node_modules/
is followed by a @
or a [a-z0-9]
character. xargs du -sh
: For each line, run du -sh
on it. That's like doing cd some/directory && du -sh
, where du
means "disk usage" and -s
means total and -h
means human-readable.sort -hr
: Sort by the first column as a "human numeric sort" meaning it understands that "1M" is more than "20K"Now, if I want to free up some disk space, I can look through the list and if I recognize a project I almost never work on any more, I just send it to rm -fr
.