From d4ba3e2ddfd8437024bc6d982bb6333364df8de7 Mon Sep 17 00:00:00 2001 From: Jakub Zych Date: Wed, 4 Nov 2026 22:54:18 +0100 Subject: [PATCH] feat: check-git-install --check-links refuses local dependencies - --check-links DIR... reports every link:, file:, workspace: or portal: dependency in each DIR/package.json and exits 1 (L4 CR-01) - the tag mode runs the same check on the installed layer before building - --self-test adds planted package.json cases next to the lockfile case - README Releasing: swap the link for the seo tag pin, run --check-links, then the tag mode after pushing --- README.md | 8 ++- scripts/check-git-install.sh | 94 +++++++++++++++++++++++++++++++++--- 2 files changed, 95 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index d3c87c8..caae2cd 100644 --- a/README.md +++ b/README.md @@ -280,10 +280,16 @@ To work on the layer inside an app, link it instead of installing from git: `pnp A release is an annotated git tag (`v0.1.0`) on `main`, pushed to git.golem15.com. Apps pin it in `package.json` as `git+https://git.golem15.com/golem15/vue-journal-layer.git#`, and `pnpm-lock.yaml` records the tag's commit. To upgrade, change the tag and run `pnpm install`. The repository is public, so installing needs no credentials. The layer has no install-time build or `prepare` script, which a git-hosted dependency would need to run during `pnpm install`. +A content layer (this one, vue-menu-layer, vue-faq-layer) extends vue-seo-layer through its `vue-seo-layer` dependency, which points at the local checkout (`link:../vue-seo-layer`) during development. A tag cut with that link breaks every git install: the path resolves next to the pnpm store entry, not to a package. Before tagging any content layer: + +1. Tag vue-seo-layer first, then in each content layer replace the link with the tag pin (`"vue-seo-layer": "git+https://git.golem15.com/golem15/vue-seo-layer.git#v0.2.0"`) and run `pnpm install`, so `pnpm-lock.yaml` records the seo layer's commit. +2. Run `bash scripts/check-git-install.sh --check-links ../vue-seo-layer ../vue-journal-layer ../vue-menu-layer ../vue-faq-layer`. It fails while any of those `package.json` files has a `link:`, `file:`, `workspace:` or `portal:` dependency, and names each one. +3. Tag and push, then run `bash scripts/check-git-install.sh `, which repeats the check on the installed tag before building. + ## Testing - `pnpm test` runs the unit tests in `tests/*.test.ts` with `node --test` (Node strips the TypeScript types; no test framework dependency). They cover the pure helpers in `app/utils/`: route normalization and path filling, the client against a fake fetcher (including the suggest query), the search helpers (term rules, diacritic-insensitive marking, plural forms, combobox keys, and the suggestion controller's debounce, abort and stale-answer handling under mocked timers), pagination, the HTML sanitizer (the Journal plugin's FormatHTML cases, the bypasses from its code review, and the Blog Editor corpus in `tests/fixtures/editor-corpus.json`, a byte copy of the plugin's), the H1 demotion and the body pipeline order, the featured image srcsets (`tests/srcset.test.ts`), the embed expansion and its sanitize-first order, the post page view, SEO values (fallback meta, the title element, ItemList), path-form routes, the old purge route's helpers and browser revalidation (`tests/siteCache.test.ts`), and pl/en key and placeholder parity. The shared purge route and the body link steps are tested in vue-seo-layer. They need no build, network or database. -- `scripts/check-git-install.sh ` installs the layer from the git tag into a scratch app built from the layer's playground config (`.playground/nuxt.config.ts` extending the installed layer, and its `app.vue`), checks that `pnpm-lock.yaml` pins the tag's commit for vue-journal-layer (its own git dependencies, such as vue-seo-layer, are ignored), runs `nuxt build`, and runs the SSR smoke against the build. `--self-test` checks the lockfile reading only. Run it from a directory that is not inside a directory named `vue-journal-layer` (Nuxt treats such a path segment as the layer). +- `scripts/check-git-install.sh ` installs the layer from the git tag into a scratch app built from the layer's playground config (`.playground/nuxt.config.ts` extending the installed layer, and its `app.vue`), checks that `pnpm-lock.yaml` pins the tag's commit for vue-journal-layer (its own git dependencies, such as vue-seo-layer, are ignored), checks that the installed `package.json` has no local dependency, runs `nuxt build`, and runs the SSR smoke against the build. `--check-links DIR...` checks the `package.json` of each directory for `link:`, `file:`, `workspace:` and `portal:` dependencies (the release guard, see [Releasing](#releasing)). `--self-test` checks the lockfile reading and the link check on planted files. Run it from a directory that is not inside a directory named `vue-journal-layer` (Nuxt treats such a path segment as the layer). - `testing/smoke-ssr.mjs --output <.output> --fixtures --expect [--env KEY=VALUE]...` boots a fixture Journal and SEO API and the built server and checks each line of the expectations file: `STATUS PATH [contains=TEXT] [notcontains=TEXT] [count=TEXT:N] [type=PREFIX] [header=NAME: VALUE] [noheader=NAME] [headings=strict] [cardlinks=N]`, every option repeatable, and `resolves PATH N` (the SEO resolve calls so far for a path, `*` for all). `headings=strict` wants exactly one ` --fixtures --kind post|category --slug --path ` proves the [site cache purge](#site-cache-purge) on a built server whose route rules cache pages: it caches the page, changes the entity in a temporary copy of the fixtures, requires the cached page to stay unchanged, purges with a random token, and requires the change on the next request. It also checks `max-age=0` on the page and its 304, the purged `_payload.json`, a second purge after re-caching, `401` without or with a wrong token, `404` behind a proxy and `404` on a server without a token. The last line is `purge-check: ok ( checks)`. - `testing/purge-token-check.mjs --output <.output> --fixtures --path ` proves on a built server that the purge token never reaches a page. It starts the server three times with fresh random tokens: under `NUXT_CACHE_PURGE_TOKEN`, under the deprecated `NUXT_SITE_CACHE_PURGE_TOKEN` with `NUXT_SITE_CACHE_ALLOW_PROXIED=true`, and under both names with different values. Each time `/`, the page and its `_payload.json` (when served) must contain no token value and neither `cachePurgeToken` nor `cacheAllowProxied`, the shared purge route must accept the expected bearer and refuse the others, and the deprecation warning must appear once with the old names only. The last line is `purge-token-check: ok ( checks)`. diff --git a/scripts/check-git-install.sh b/scripts/check-git-install.sh index 422785a..a072121 100755 --- a/scripts/check-git-install.sh +++ b/scripts/check-git-install.sh @@ -1,12 +1,23 @@ #!/usr/bin/env bash # Usage: scripts/check-git-install.sh -# scripts/check-git-install.sh --self-test (the lockfile check only) +# scripts/check-git-install.sh --check-links DIR... +# scripts/check-git-install.sh --self-test (the lockfile and link checks only) # # Proves the layer's distribution end to end: a scratch app installs # vue-journal-layer from git+https://git.golem15.com/golem15/vue-journal-layer.git# -# anonymously, the lockfile pins the tag's commit, `nuxt build` of the layer's -# playground config succeeds, and the built server passes -# testing/expect-acme.txt against the fixture API. +# anonymously, the installed package.json has no local dependency, the +# lockfile pins the tag's commit, `nuxt build` of the layer's playground +# config succeeds, and the built server passes testing/expect-acme.txt +# against the fixture API. +# +# --check-links is the release guard to run before tagging any content +# layer (README, Releasing): it reads DIR/package.json for each DIR and +# prints `check-git-install: : depends on through ` for +# every dependencies, devDependencies, peerDependencies or +# optionalDependencies entry whose spec starts with link:, file:, +# workspace: or portal: (a path that resolves outside the package once it +# is installed from git), exiting 1 when it finds any or cannot read a +# package.json. # Nothing outside a temporary directory is touched. set -euo pipefail @@ -24,9 +35,55 @@ lock_commits() { } | sort -u } +# local_links LABEL DIR... prints one line per local dependency of each +# DIR/package.json (prefixed with LABEL, else the dir) and fails when there +# is any, or when a package.json cannot be read. +local_links() { + local label="$1" + shift + node -e ' + const fs = require("node:fs") + const path = require("node:path") + const [label, ...dirs] = process.argv.slice(1) + const LOCAL = /^(link|file|workspace|portal):/ + let bad = false + for (const dir of dirs) { + const who = label || dir + let pkg + try { + pkg = JSON.parse(fs.readFileSync(path.join(dir, "package.json"), "utf8")) + } catch (err) { + console.error(`check-git-install: ${who}: cannot read package.json (${err.message})`) + bad = true + continue + } + for (const section of ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"]) { + const deps = pkg[section] && typeof pkg[section] === "object" ? pkg[section] : {} + for (const [name, spec] of Object.entries(deps)) { + if (typeof spec === "string" && LOCAL.test(spec)) { + console.error(`check-git-install: ${who}: depends on ${name} through ${spec} (${section})`) + bad = true + } + } + } + } + process.exit(bad ? 1 : 0) + ' "$label" "$@" +} + +if [[ "${1:-}" == "--check-links" ]]; then + shift + [[ $# -gt 0 ]] || die "usage: $0 --check-links DIR..." + command -v node >/dev/null 2>&1 || die "node is not installed" + local_links "" "$@" || exit 1 + echo "check-git-install: no local dependencies in $*" + exit 0 +fi + if [[ "${1:-}" == "--self-test" ]]; then lock="$(mktemp)" - trap 'rm -f "$lock"' EXIT + pkgs="$(mktemp -d)" + trap 'rm -f "$lock"; rm -rf "$pkgs"' EXIT j=1111111111111111111111111111111111111111 s=2222222222222222222222222222222222222222 cat >"$lock" <"$pkgs/linked/package.json" <<'JSON' +{ "name": "vue-menu-layer", "dependencies": { "vue-seo-layer": "link:../vue-seo-layer", "nuxt": "4.4.8" } } +JSON + cat >"$pkgs/pinned/package.json" <<'JSON' +{ "name": "vue-menu-layer", "dependencies": { "vue-seo-layer": "git+https://git.golem15.com/golem15/vue-seo-layer.git#v0.2.0", "nuxt": "4.4.8" } } +JSON + cat >"$pkgs/dev/package.json" <<'JSON' +{ "name": "x", "devDependencies": { "a": "file:../a" }, "optionalDependencies": { "b": "workspace:*" }, "peerDependencies": { "c": "portal:../c" } } +JSON + if out="$(local_links "" "$pkgs/linked" 2>&1)"; then die "self-test: a link: dependency passed"; fi + [[ "$out" == "check-git-install: $pkgs/linked: depends on vue-seo-layer through link:../vue-seo-layer (dependencies)" ]] \ + || die "self-test: the link: report was: $out" + out="$(local_links "" "$pkgs/pinned" 2>&1)" || die "self-test: a git+https pin failed: $out" + [[ -z "$out" ]] || die "self-test: a git+https pin printed: $out" + if out="$(local_links "" "$pkgs/dev" 2>&1)"; then die "self-test: file:, workspace: and portal: dependencies passed"; fi + [[ "$(grep -c 'depends on' <<<"$out")" -eq 3 ]] || die "self-test: want three reports for file:, workspace: and portal:, got: $out" + if out="$(local_links "the tag's package.json" "$pkgs/missing" 2>&1)"; then die "self-test: a missing package.json passed"; fi + if out="$(local_links "" "$pkgs/pinned" "$pkgs/linked" 2>&1)"; then die "self-test: one linked package among several passed"; fi echo "check-git-install: self-test ok" exit 0 fi @@ -89,7 +166,12 @@ allowBuilds: vue-demi: true YAML -GIT_TERMINAL_PROMPT=0 pnpm install || die "pnpm install failed" +GIT_TERMINAL_PROMPT=0 pnpm install || die "pnpm install failed (a link: or file: dependency in the tag's package.json fails here too; run --check-links before tagging)" + +# The tag must not carry a local dependency (link:, file:, workspace:, +# portal:): it would resolve next to the pnpm store entry, not to a package. +local_links "the tag's package.json" node_modules/vue-journal-layer \ + || die "the tag's package.json still has a local dependency (see above); pin it to a git+https tag before tagging" # The lockfile must pin the tag's commit, not a branch. Only the # vue-journal-layer entries count: the layer's own git dependencies