#!/usr/bin/env bash # Usage: scripts/check-git-install.sh # 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, next to vue-seo-layer (a peer dependency, listed by the app at # the tag's devDependencies pin) with pnpm's default settings, 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 REPO_URL="https://git.golem15.com/golem15/vue-journal-layer.git" die() { echo "check-git-install: $*" >&2; exit 1; } # lock_commits FILE prints the commits FILE pins for vue-journal-layer, one # per line: the # of its git+https entries and the commit of a # resolution whose repo is the layer's. Other git dependencies are ignored. lock_commits() { { grep -oE 'vue-journal-layer\.git#[0-9a-f]{40}' "$1" | cut -d'#' -f2 grep -oE 'commit: [0-9a-f]{40}, repo: [^,}]*/vue-journal-layer\.git' "$1" | grep -oE '[0-9a-f]{40}' } | 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)" 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 TAG="${1:-}" [[ -n "$TAG" ]] || die "usage: $0 " for tool in git pnpm node; do command -v "$tool" >/dev/null 2>&1 || die "$tool is not installed" done WORK="$(mktemp -d "${TMPDIR:-/tmp}/journal-git-install.XXXXXX")" # Nuxt applies its auto-import transform to any path with a # /vue-journal-layer/ segment (how it finds a layer inside node_modules), so a # scratch app below such a directory would transform every dependency. case "$WORK/" in */vue-journal-layer/*) rm -rf "$WORK"; die "TMPDIR must not be inside a directory named vue-journal-layer" ;; esac cleanup() { rm -rf "$WORK"; } trap cleanup EXIT # The commit the tag points at (peeled when annotated). REMOTE_REFS="$(GIT_TERMINAL_PROMPT=0 git ls-remote "$REPO_URL" "refs/tags/$TAG" "refs/tags/$TAG^{}")" \ || die "git ls-remote $REPO_URL failed" TAG_SHA="$(awk -v ref="refs/tags/$TAG^{}" '$2 == ref { print $1 }' <<<"$REMOTE_REFS")" [[ -n "$TAG_SHA" ]] || TAG_SHA="$(awk -v ref="refs/tags/$TAG" '$2 == ref { print $1 }' <<<"$REMOTE_REFS")" [[ -n "$TAG_SHA" ]] || die "tag $TAG not found on $REPO_URL" # vue-seo-layer is a peer dependency of the layer, so an app lists it next to # the layer, as the README's Installation says. The scratch app takes the # spec the tag pins for its own development (devDependencies), or # SEO_LAYER_SPEC when set. A tag that still lists vue-seo-layer under # dependencies (v0.8.0) gets none: pnpm then refuses it as an exotic # subdependency, which is what this check is for. SEO_SPEC="${SEO_LAYER_SPEC:-}" if [[ -z "$SEO_SPEC" ]]; then SRC="$(mktemp -d "${TMPDIR:-/tmp}/journal-git-install-src.XXXXXX")" GIT_TERMINAL_PROMPT=0 git clone --quiet --depth 1 --branch "$TAG" "$REPO_URL" "$SRC/layer" 2>/dev/null \ || { rm -rf "$SRC"; die "git clone of $TAG failed"; } SEO_SPEC="$(node -e ' const pkg = JSON.parse(require("node:fs").readFileSync(process.argv[1], "utf8")) const peer = (pkg.peerDependencies || {})["vue-seo-layer"] const dev = (pkg.devDependencies || {})["vue-seo-layer"] process.stdout.write(peer && dev ? dev : "") ' "$SRC/layer/package.json")" || { rm -rf "$SRC"; die "cannot read the tag's package.json"; } rm -rf "$SRC" fi cd "$WORK" node -e ' const [repo, tag, seo] = process.argv.slice(1) const deps = { nuxt: "4.4.8", vue: "3.5.35" } if (seo) deps["vue-seo-layer"] = seo deps["vue-journal-layer"] = `git+${repo}#${tag}` const pkg = { name: "acme-blog", private: true, type: "module", dependencies: deps } require("node:fs").writeFileSync("package.json", JSON.stringify(pkg, null, 2) + "\n") ' "$REPO_URL" "$TAG" "$SEO_SPEC" cat >pnpm-workspace.yaml <<'YAML' allowBuilds: '@parcel/watcher': true esbuild: true vue-demi: true YAML 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 # (vue-seo-layer) are pinned in the same file. LOCK_SHA="$(lock_commits pnpm-lock.yaml)" [[ -n "$LOCK_SHA" ]] || die "pnpm-lock.yaml records no commit for vue-journal-layer" [[ "$(wc -l <<<"$LOCK_SHA")" -eq 1 ]] || die "pnpm-lock.yaml records more than one vue-journal-layer commit: $LOCK_SHA" [[ "$LOCK_SHA" == "$TAG_SHA" ]] || die "pnpm-lock.yaml pins $LOCK_SHA, tag $TAG is $TAG_SHA" # The scratch app is the layer's playground (its journal options, such as # the search route and one post per category page, and its app.vue with the # search box and the layout trail), which testing/expect-acme.txt is # written against, extending the installed layer instead of '..'. PLAYGROUND=node_modules/vue-journal-layer/.playground [[ -f "$PLAYGROUND/nuxt.config.ts" && -f "$PLAYGROUND/app/app.vue" ]] \ || die "the installed layer has no .playground/nuxt.config.ts and .playground/app/app.vue" sed "s#extends: \['\.\.'\]#extends: ['vue-journal-layer']#" "$PLAYGROUND/nuxt.config.ts" >nuxt.config.ts grep -q "extends: \['vue-journal-layer'\]" nuxt.config.ts || die "the playground's nuxt.config.ts does not extend '..'" mkdir -p app cp "$PLAYGROUND/app/app.vue" app/app.vue pnpm exec nuxt build || die "nuxt build failed" node node_modules/vue-journal-layer/testing/smoke-ssr.mjs \ --output .output \ --fixtures node_modules/vue-journal-layer/testing/fixtures/acme \ --expect node_modules/vue-journal-layer/testing/expect-acme.txt \ || die "SSR smoke failed" echo "check-git-install: ok $TAG $TAG_SHA"