From 08648b81374bbde5bf79918f8619f2fc810c5496 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 29 Sep 2026 01:41:32 +0000 Subject: [PATCH 01/10] feat(lint): add linter to reject non-yarn Node.js lockfiles Adds shell/linters/nodejs-lockfile.sh, which fails when package-lock.json, npm-shrinkwrap.json, pnpm-lock.yaml, bun.lock, or bun.lockb are present, keeping yarn.lock as the only supported lockfile for Node.js dependencies. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01NApzycVfBg7yxzWQEjU2G7 --- shell/linters/nodejs-lockfile.sh | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100755 shell/linters/nodejs-lockfile.sh diff --git a/shell/linters/nodejs-lockfile.sh b/shell/linters/nodejs-lockfile.sh new file mode 100755 index 00000000..857ff4c3 --- /dev/null +++ b/shell/linters/nodejs-lockfile.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +# Prevents lockfiles from Node.js package managers other than yarn (npm, +# pnpm, bun) from being committed. yarn.lock is the only supported lockfile. + +# Why: Used by the script that calls us +# shellcheck disable=SC2034 +extensions=(json yaml lock lockb) + +# forbidden_lockfile_pattern matches lockfiles produced by Node.js package +# managers other than yarn. +forbidden_lockfile_pattern='(^|/)(package-lock\.json|npm-shrinkwrap\.json|pnpm-lock\.yaml|bun\.lock|bun\.lockb)$' + +lockfile_linter() { + local found + found="$(find_files_with_extensions "${extensions[@]}" | grep -E "$forbidden_lockfile_pattern" || true)" + + if [[ -n $found ]]; then + error "Only yarn.lock is supported for Node.js dependencies. Remove the following lockfile(s) and use 'yarn install' instead:" + echo "$found" >&2 + return 1 + fi + + return 0 +} + +linter() { + run_command "node-lockfile" lockfile_linter || return 1 +} + +formatter() { + true # No formatter for this linter +} From e5626b26a1f1cf68c996bb91fe9ec5be1983886a Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 29 Sep 2026 02:33:06 +0000 Subject: [PATCH 02/10] feat(lint): require yarn.lock for the generated Node.js API client api/clients/node/ is stencil's generated gRPC Node.js client, and its CircleCI jobs run `yarn --frozen-lockfile`, which fails without a committed yarn.lock. Extend shell/linters/nodejs-lockfile.sh to fail when api/clients/node/package.json exists without a sibling yarn.lock. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01NApzycVfBg7yxzWQEjU2G7 --- shell/linters/nodejs-lockfile.sh | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/shell/linters/nodejs-lockfile.sh b/shell/linters/nodejs-lockfile.sh index 857ff4c3..e1556bb1 100755 --- a/shell/linters/nodejs-lockfile.sh +++ b/shell/linters/nodejs-lockfile.sh @@ -1,6 +1,7 @@ #!/usr/bin/env bash # Prevents lockfiles from Node.js package managers other than yarn (npm, -# pnpm, bun) from being committed. yarn.lock is the only supported lockfile. +# pnpm, bun) from being committed, and ensures yarn.lock is committed for +# well-known generated Node.js packages. # Why: Used by the script that calls us # shellcheck disable=SC2034 @@ -10,6 +11,10 @@ extensions=(json yaml lock lockb) # managers other than yarn. forbidden_lockfile_pattern='(^|/)(package-lock\.json|npm-shrinkwrap\.json|pnpm-lock\.yaml|bun\.lock|bun\.lockb)$' +# node_client_dirs lists generated Node.js package directories that must +# ship a committed yarn.lock alongside their package.json. +node_client_dirs=(api/clients/node) + lockfile_linter() { local found found="$(find_files_with_extensions "${extensions[@]}" | grep -E "$forbidden_lockfile_pattern" || true)" @@ -23,8 +28,28 @@ lockfile_linter() { return 0 } +yarn_lock_presence_linter() { + local dir + local missing=() + + for dir in "${node_client_dirs[@]}"; do + if [[ -f "$dir/package.json" && ! -f "$dir/yarn.lock" ]]; then + missing+=("$dir/yarn.lock") + fi + done + + if [[ ${#missing[@]} -gt 0 ]]; then + error "Missing yarn.lock for the following Node.js package(s):" + printf '%s\n' "${missing[@]}" >&2 + return 1 + fi + + return 0 +} + linter() { run_command "node-lockfile" lockfile_linter || return 1 + run_command "node-yarn-lock-presence" yarn_lock_presence_linter || return 1 } formatter() { From a0c93b30a2c640feb137aa276fe4a49d8d9df3e3 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 29 Sep 2026 02:37:12 +0000 Subject: [PATCH 03/10] fix(lint): also require yarn.lock at the repo root yarn_lock_presence_linter only checked api/clients/node, missing the common case of a root-level package.json (this repo included) with no committed yarn.lock. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01NApzycVfBg7yxzWQEjU2G7 --- shell/linters/nodejs-lockfile.sh | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/shell/linters/nodejs-lockfile.sh b/shell/linters/nodejs-lockfile.sh index e1556bb1..3418128a 100755 --- a/shell/linters/nodejs-lockfile.sh +++ b/shell/linters/nodejs-lockfile.sh @@ -11,9 +11,10 @@ extensions=(json yaml lock lockb) # managers other than yarn. forbidden_lockfile_pattern='(^|/)(package-lock\.json|npm-shrinkwrap\.json|pnpm-lock\.yaml|bun\.lock|bun\.lockb)$' -# node_client_dirs lists generated Node.js package directories that must -# ship a committed yarn.lock alongside their package.json. -node_client_dirs=(api/clients/node) +# node_client_dirs lists Node.js package directories that must ship a +# committed yarn.lock alongside their package.json: the repo root, and +# stencil's generated gRPC Node.js client. +node_client_dirs=(. api/clients/node) lockfile_linter() { local found From 6c0b2143389212288c7b87467eadc6548cc83873 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 29 Sep 2026 02:38:30 +0000 Subject: [PATCH 04/10] refactor(lint): rename node_client_dirs to nodejs_client_dirs Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01NApzycVfBg7yxzWQEjU2G7 --- shell/linters/nodejs-lockfile.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/shell/linters/nodejs-lockfile.sh b/shell/linters/nodejs-lockfile.sh index 3418128a..0f529d91 100755 --- a/shell/linters/nodejs-lockfile.sh +++ b/shell/linters/nodejs-lockfile.sh @@ -11,10 +11,10 @@ extensions=(json yaml lock lockb) # managers other than yarn. forbidden_lockfile_pattern='(^|/)(package-lock\.json|npm-shrinkwrap\.json|pnpm-lock\.yaml|bun\.lock|bun\.lockb)$' -# node_client_dirs lists Node.js package directories that must ship a +# nodejs_client_dirs lists Node.js package directories that must ship a # committed yarn.lock alongside their package.json: the repo root, and # stencil's generated gRPC Node.js client. -node_client_dirs=(. api/clients/node) +nodejs_client_dirs=(. api/clients/node) lockfile_linter() { local found @@ -33,7 +33,7 @@ yarn_lock_presence_linter() { local dir local missing=() - for dir in "${node_client_dirs[@]}"; do + for dir in "${nodejs_client_dirs[@]}"; do if [[ -f "$dir/package.json" && ! -f "$dir/yarn.lock" ]]; then missing+=("$dir/yarn.lock") fi From 5d97fbc5a208cce40aecd5f6a71be032a0dd4be5 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 29 Sep 2026 02:39:23 +0000 Subject: [PATCH 05/10] refactor(lint): rename nodejs_client_dirs to nodejs_dirs Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01NApzycVfBg7yxzWQEjU2G7 --- shell/linters/nodejs-lockfile.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/shell/linters/nodejs-lockfile.sh b/shell/linters/nodejs-lockfile.sh index 0f529d91..6b44c7f7 100755 --- a/shell/linters/nodejs-lockfile.sh +++ b/shell/linters/nodejs-lockfile.sh @@ -11,10 +11,10 @@ extensions=(json yaml lock lockb) # managers other than yarn. forbidden_lockfile_pattern='(^|/)(package-lock\.json|npm-shrinkwrap\.json|pnpm-lock\.yaml|bun\.lock|bun\.lockb)$' -# nodejs_client_dirs lists Node.js package directories that must ship a +# nodejs_dirs lists Node.js package directories that must ship a # committed yarn.lock alongside their package.json: the repo root, and # stencil's generated gRPC Node.js client. -nodejs_client_dirs=(. api/clients/node) +nodejs_dirs=(. api/clients/node) lockfile_linter() { local found @@ -33,7 +33,7 @@ yarn_lock_presence_linter() { local dir local missing=() - for dir in "${nodejs_client_dirs[@]}"; do + for dir in "${nodejs_dirs[@]}"; do if [[ -f "$dir/package.json" && ! -f "$dir/yarn.lock" ]]; then missing+=("$dir/yarn.lock") fi From de362d46f8f211c9e3964689d7aa03fad58ef2b3 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 29 Sep 2026 02:42:19 +0000 Subject: [PATCH 06/10] refactor(lint): scope lockfile_linter to known Node.js package dirs Check for forbidden lockfiles only within nodejs_dirs (repo root and api/clients/node) instead of scanning the whole repo tree, matching the scope yarn_lock_presence_linter already uses. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01NApzycVfBg7yxzWQEjU2G7 --- shell/linters/nodejs-lockfile.sh | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/shell/linters/nodejs-lockfile.sh b/shell/linters/nodejs-lockfile.sh index 6b44c7f7..37348d5b 100755 --- a/shell/linters/nodejs-lockfile.sh +++ b/shell/linters/nodejs-lockfile.sh @@ -7,22 +7,31 @@ # shellcheck disable=SC2034 extensions=(json yaml lock lockb) -# forbidden_lockfile_pattern matches lockfiles produced by Node.js package -# managers other than yarn. -forbidden_lockfile_pattern='(^|/)(package-lock\.json|npm-shrinkwrap\.json|pnpm-lock\.yaml|bun\.lock|bun\.lockb)$' +# forbidden_lockfiles lists lockfiles produced by Node.js package managers +# other than yarn. +forbidden_lockfiles=(package-lock.json npm-shrinkwrap.json pnpm-lock.yaml bun.lock bun.lockb) # nodejs_dirs lists Node.js package directories that must ship a -# committed yarn.lock alongside their package.json: the repo root, and -# stencil's generated gRPC Node.js client. +# committed yarn.lock alongside their package.json, and must not contain +# a lockfile from another package manager: the repo root, and stencil's +# generated gRPC Node.js client. nodejs_dirs=(. api/clients/node) lockfile_linter() { - local found - found="$(find_files_with_extensions "${extensions[@]}" | grep -E "$forbidden_lockfile_pattern" || true)" + local dir file + local found=() - if [[ -n $found ]]; then + for dir in "${nodejs_dirs[@]}"; do + for file in "${forbidden_lockfiles[@]}"; do + if [[ -f "$dir/$file" ]]; then + found+=("$dir/$file") + fi + done + done + + if [[ ${#found[@]} -gt 0 ]]; then error "Only yarn.lock is supported for Node.js dependencies. Remove the following lockfile(s) and use 'yarn install' instead:" - echo "$found" >&2 + printf '%s\n' "${found[@]}" >&2 return 1 fi From 7627d284d39bc01cf20a2a33e183cf7b4ad2b6f5 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 29 Sep 2026 02:52:52 +0000 Subject: [PATCH 07/10] docs(lint): update header comment to match nodejs_dirs scope The header still described repo-wide lockfile enforcement after lockfile_linter was scoped to nodejs_dirs. Refer to nodejs_dirs and the forbidden_lockfiles concept by name instead of enumerating directories or package managers, so the comment can't drift out of sync with those arrays. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01NApzycVfBg7yxzWQEjU2G7 --- shell/linters/nodejs-lockfile.sh | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/shell/linters/nodejs-lockfile.sh b/shell/linters/nodejs-lockfile.sh index 37348d5b..78c930cc 100755 --- a/shell/linters/nodejs-lockfile.sh +++ b/shell/linters/nodejs-lockfile.sh @@ -1,7 +1,8 @@ #!/usr/bin/env bash -# Prevents lockfiles from Node.js package managers other than yarn (npm, -# pnpm, bun) from being committed, and ensures yarn.lock is committed for -# well-known generated Node.js packages. +# Ensures yarn is the only package manager used for Node.js dependencies +# in nodejs_dirs, the only locations where stencil manages a Node.js +# package.json. Fails if a lockfile from another package manager is +# present, or if yarn.lock is missing. # Why: Used by the script that calls us # shellcheck disable=SC2034 From c01a35a6be2584e69e3b9953aca0bac3e1fafc90 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 29 Sep 2026 02:55:29 +0000 Subject: [PATCH 08/10] refactor(lint): genericize required package manager lockfile Replace the hardcoded forbidden_lockfiles list and yarn-specific presence check with all_lockfiles + required_lockfile: forbidden lockfiles are derived by excluding required_lockfile from all_lockfiles. Switching the required Node.js package manager (e.g. yarn.lock to pnpm-lock.yaml) is now a one-line change. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01NApzycVfBg7yxzWQEjU2G7 --- shell/linters/nodejs-lockfile.sh | 39 ++++++++++++++++++-------------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/shell/linters/nodejs-lockfile.sh b/shell/linters/nodejs-lockfile.sh index 78c930cc..25d54bdf 100755 --- a/shell/linters/nodejs-lockfile.sh +++ b/shell/linters/nodejs-lockfile.sh @@ -1,21 +1,25 @@ #!/usr/bin/env bash -# Ensures yarn is the only package manager used for Node.js dependencies -# in nodejs_dirs, the only locations where stencil manages a Node.js -# package.json. Fails if a lockfile from another package manager is -# present, or if yarn.lock is missing. +# Ensures required_lockfile is the only package manager lockfile used +# for Node.js dependencies in nodejs_dirs, the only locations where +# stencil manages a Node.js package.json. Fails if a lockfile from +# another package manager is present, or if required_lockfile is +# missing. # Why: Used by the script that calls us # shellcheck disable=SC2034 extensions=(json yaml lock lockb) -# forbidden_lockfiles lists lockfiles produced by Node.js package managers -# other than yarn. -forbidden_lockfiles=(package-lock.json npm-shrinkwrap.json pnpm-lock.yaml bun.lock bun.lockb) +# all_lockfiles lists the lockfile produced by every Node.js package +# manager this linter knows about. required_lockfile is the one +# nodejs_dirs must commit; every other entry is forbidden. Changing the +# required package manager only means updating required_lockfile. +all_lockfiles=(yarn.lock package-lock.json npm-shrinkwrap.json pnpm-lock.yaml bun.lock bun.lockb) +required_lockfile=yarn.lock # nodejs_dirs lists Node.js package directories that must ship a -# committed yarn.lock alongside their package.json, and must not contain -# a lockfile from another package manager: the repo root, and stencil's -# generated gRPC Node.js client. +# committed copy of required_lockfile alongside their package.json, and +# must not contain a lockfile from another package manager: the repo +# root, and stencil's generated gRPC Node.js client. nodejs_dirs=(. api/clients/node) lockfile_linter() { @@ -23,7 +27,8 @@ lockfile_linter() { local found=() for dir in "${nodejs_dirs[@]}"; do - for file in "${forbidden_lockfiles[@]}"; do + for file in "${all_lockfiles[@]}"; do + [[ $file == "$required_lockfile" ]] && continue if [[ -f "$dir/$file" ]]; then found+=("$dir/$file") fi @@ -31,7 +36,7 @@ lockfile_linter() { done if [[ ${#found[@]} -gt 0 ]]; then - error "Only yarn.lock is supported for Node.js dependencies. Remove the following lockfile(s) and use 'yarn install' instead:" + error "Only $required_lockfile is supported for Node.js dependencies. Remove the following lockfile(s):" printf '%s\n' "${found[@]}" >&2 return 1 fi @@ -39,18 +44,18 @@ lockfile_linter() { return 0 } -yarn_lock_presence_linter() { +required_lockfile_presence_linter() { local dir local missing=() for dir in "${nodejs_dirs[@]}"; do - if [[ -f "$dir/package.json" && ! -f "$dir/yarn.lock" ]]; then - missing+=("$dir/yarn.lock") + if [[ -f "$dir/package.json" && ! -f "$dir/$required_lockfile" ]]; then + missing+=("$dir/$required_lockfile") fi done if [[ ${#missing[@]} -gt 0 ]]; then - error "Missing yarn.lock for the following Node.js package(s):" + error "Missing $required_lockfile for the following Node.js package(s):" printf '%s\n' "${missing[@]}" >&2 return 1 fi @@ -60,7 +65,7 @@ yarn_lock_presence_linter() { linter() { run_command "node-lockfile" lockfile_linter || return 1 - run_command "node-yarn-lock-presence" yarn_lock_presence_linter || return 1 + run_command "node-lockfile-presence" required_lockfile_presence_linter || return 1 } formatter() { From ece554f0ef3b474222418664dc09c4e70696e724 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 29 Sep 2026 02:57:46 +0000 Subject: [PATCH 09/10] refactor(lint): rename node-lockfile run_command label to nodejs-lockfile Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01NApzycVfBg7yxzWQEjU2G7 --- shell/linters/nodejs-lockfile.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shell/linters/nodejs-lockfile.sh b/shell/linters/nodejs-lockfile.sh index 25d54bdf..cdf56dec 100755 --- a/shell/linters/nodejs-lockfile.sh +++ b/shell/linters/nodejs-lockfile.sh @@ -64,7 +64,7 @@ required_lockfile_presence_linter() { } linter() { - run_command "node-lockfile" lockfile_linter || return 1 + run_command "nodejs-lockfile" lockfile_linter || return 1 run_command "node-lockfile-presence" required_lockfile_presence_linter || return 1 } From 6401810f02818ef889615bb0b10ec2051ebba727 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 29 Sep 2026 02:58:40 +0000 Subject: [PATCH 10/10] refactor(lint): rename node-lockfile-presence run_command label to nodejs-lockfile-presence Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01NApzycVfBg7yxzWQEjU2G7 --- shell/linters/nodejs-lockfile.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shell/linters/nodejs-lockfile.sh b/shell/linters/nodejs-lockfile.sh index cdf56dec..1f99e9fc 100755 --- a/shell/linters/nodejs-lockfile.sh +++ b/shell/linters/nodejs-lockfile.sh @@ -65,7 +65,7 @@ required_lockfile_presence_linter() { linter() { run_command "nodejs-lockfile" lockfile_linter || return 1 - run_command "node-lockfile-presence" required_lockfile_presence_linter || return 1 + run_command "nodejs-lockfile-presence" required_lockfile_presence_linter || return 1 } formatter() {