From 9f2637becfdb91cf75573b88bb1ca6d45d81d3ba Mon Sep 17 00:00:00 2001 From: Avner Matan <137777701+avner-m@users.noreply.github.com> Date: Tue, 13 Oct 2026 15:10:48 +1300 Subject: [PATCH 1/3] build(ci): add weekly dependency refresh, grouped Dependabot and app CI gate --- .github/dependabot.yml | 166 +++++++++++++ .github/workflows/ci-apps.yml | 83 +++++++ .github/workflows/deps-weekly-refresh.yml | 57 +++++ README.md | 11 + scripts/deps/lockfile.mjs | 146 ++++++++++++ scripts/deps/lockfile.test.mjs | 114 +++++++++ scripts/deps/refresh.mjs | 272 ++++++++++++++++++++++ 7 files changed, 849 insertions(+) create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/ci-apps.yml create mode 100644 .github/workflows/deps-weekly-refresh.yml create mode 100644 scripts/deps/lockfile.mjs create mode 100644 scripts/deps/lockfile.test.mjs create mode 100644 scripts/deps/refresh.mjs diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..f7c451f --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,166 @@ +version: 2 + +# Minor and patch bumps inside each app's existing ranges are handled by the weekly +# refresh workflow (deps-weekly-refresh.yml), which updates every lockfile in one PR. +# Dependabot only opens what the refresh can't do: majors, and security fixes. + +registries: + npm-mattr: + type: npm-registry + url: https://registry.npmjs.org + token: ${{ secrets.NPM_TOKEN }} + +updates: + # Node and Next.js apps + - package-ecosystem: npm + directories: + - "/claims-source-app" + - "/interaction-hook-app" + - "/manual-revocation-check" + - "/verifier-web-tutorial" + - "/webhook-app" + schedule: + interval: weekly + day: monday + time: "09:00" + timezone: Pacific/Auckland + open-pull-requests-limit: 8 + commit-message: + prefix: "build(deps)" + prefix-development: "build(deps-dev)" + groups: + security: + applies-to: security-updates + patterns: + - "*" + # Version-locked to each other, so they move together. + next-react-major: + patterns: + - "next" + - "eslint-config-next" + - "react" + - "react-dom" + - "@types/react" + - "@types/react-dom" + update-types: + - major + # Same dependency across several apps lands in one PR. + majors: + group-by: dependency-name + patterns: + - "*" + update-types: + - major + ignore: + - dependency-name: "*" + update-types: + - version-update:semver-minor + - version-update:semver-patch + + # React Native (Expo) tutorials, starter and complete + - package-ecosystem: npm + directories: + - "/react-native-in-person-verifier-tutorial/react-native-in-person-verifier-tutorial-complete" + - "/react-native-in-person-verifier-tutorial/react-native-in-person-verifier-tutorial-starter" + - "/react-native-mdocs-holder-tutorial/react-native-mdocs-holder-tutorial-complete" + - "/react-native-mdocs-holder-tutorial/react-native-mdocs-holder-tutorial-starter" + - "/react-native-remote-verification-tutorial/react-native-remote-verification-tutorial-complete" + - "/react-native-remote-verification-tutorial/react-native-remote-verification-tutorial-starter" + registries: + - npm-mattr + schedule: + interval: weekly + day: monday + time: "09:00" + timezone: Pacific/Auckland + open-pull-requests-limit: 5 + commit-message: + prefix: "build(deps)" + prefix-development: "build(deps-dev)" + groups: + security: + applies-to: security-updates + patterns: + - "*" + rn-tooling-major: + group-by: dependency-name + patterns: + - "*" + update-types: + - major + ignore: + - dependency-name: "*" + update-types: + - version-update:semver-minor + - version-update:semver-patch + # Expo SDK, React Native and MATTR SDK majors are upgraded deliberately, together + # with the matching Learn tutorial (`npx expo install --fix`), never one at a time. + - dependency-name: "expo" + update-types: ["version-update:semver-major"] + - dependency-name: "expo-*" + update-types: ["version-update:semver-major"] + - dependency-name: "@expo/*" + update-types: ["version-update:semver-major"] + - dependency-name: "react" + update-types: ["version-update:semver-major"] + - dependency-name: "react-dom" + update-types: ["version-update:semver-major"] + - dependency-name: "react-native" + update-types: ["version-update:semver-major"] + - dependency-name: "@mattrglobal/*" + update-types: ["version-update:semver-major"] + + - package-ecosystem: gradle + directories: + - "/android-holder-tutorial-sample-app" + - "/android-in-person-verifier-tutorial-sample-app" + - "/android-remote-verification-tutorial-sample-app" + schedule: + interval: weekly + day: monday + time: "09:00" + timezone: Pacific/Auckland + open-pull-requests-limit: 3 + commit-message: + prefix: "build(deps)" + groups: + android: + group-by: dependency-name + patterns: + - "*" + ignore: + # Resolved from each app's local `repo/` folder, which isn't committed. + - dependency-name: "global.mattr.mobilecredential:*" + + - package-ecosystem: docker + directories: + - "/claims-source-app" + - "/webhook-app" + schedule: + interval: weekly + day: monday + time: "09:00" + timezone: Pacific/Auckland + open-pull-requests-limit: 2 + commit-message: + prefix: "build(deps)" + groups: + docker: + group-by: dependency-name + patterns: + - "*" + + - package-ecosystem: github-actions + directory: "/" + schedule: + interval: weekly + day: monday + time: "09:00" + timezone: Pacific/Auckland + open-pull-requests-limit: 3 + commit-message: + prefix: "build(ci)" + groups: + github-actions: + patterns: + - "*" diff --git a/.github/workflows/ci-apps.yml b/.github/workflows/ci-apps.yml new file mode 100644 index 0000000..152a105 --- /dev/null +++ b/.github/workflows/ci-apps.yml @@ -0,0 +1,83 @@ +name: CI apps + +# Installs each changed JS app from its lockfile, then builds the Next.js apps and +# type-checks the complete React Native apps. The native Android and iOS apps aren't +# built: their MATTR SDKs are added locally and aren't available in CI. + +on: + pull_request: + branches: [master] + +permissions: + contents: read + +jobs: + changed: + runs-on: ubuntu-latest + outputs: + apps: ${{ steps.apps.outputs.apps }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + persist-credentials: false + + - name: List changed apps + id: apps + env: + BASE_SHA: ${{ github.event.pull_request.base.sha }} + run: | + apps=$(git ls-files '*package-lock.json' '*yarn.lock' | xargs -n1 dirname | while read -r dir; do + if ! git diff --quiet "$BASE_SHA"...HEAD -- "$dir"; then echo "$dir"; fi + done | jq -R . | jq -sc .) + echo "apps=$apps" >> "$GITHUB_OUTPUT" + echo "Changed apps: $apps" + + check: + needs: changed + if: needs.changed.outputs.apps != '[]' + runs-on: ubuntu-latest + timeout-minutes: 20 + strategy: + fail-fast: false + matrix: + app: ${{ fromJSON(needs.changed.outputs.apps) }} + defaults: + run: + working-directory: ${{ matrix.app }} + env: + COREPACK_ENABLE_DOWNLOAD_PROMPT: "0" + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + YARN_NPM_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + NEXT_TELEMETRY_DISABLED: "1" + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: 24 + registry-url: https://registry.npmjs.org + + - name: Skip without registry access + id: access + if: startsWith(matrix.app, 'react-native-') && env.YARN_NPM_AUTH_TOKEN == '' + run: echo "::notice::NPM_TOKEN isn't available (fork PR?), skipping ${{ matrix.app }}" + + - name: Install + if: steps.access.conclusion == 'skipped' + run: | + if [ -f yarn.lock ]; then + corepack yarn install --immutable --mode=skip-build + else + npm ci --ignore-scripts --no-audit --no-fund + fi + + - name: Build + if: steps.access.conclusion == 'skipped' && hashFiles(format('{0}/package-lock.json', matrix.app)) != '' + run: npm run build --if-present + + - name: Type-check + if: steps.access.conclusion == 'skipped' && startsWith(matrix.app, 'react-native-') && endsWith(matrix.app, '-complete') + run: corepack yarn tsc --noEmit diff --git a/.github/workflows/deps-weekly-refresh.yml b/.github/workflows/deps-weekly-refresh.yml new file mode 100644 index 0000000..aa24d65 --- /dev/null +++ b/.github/workflows/deps-weekly-refresh.yml @@ -0,0 +1,57 @@ +name: Weekly dependency refresh + +# Re-resolves every app's lockfile to the newest versions its package.json ranges allow, +# and opens (or updates) a single PR whose body looks ahead at what is still pending. + +on: + schedule: + # Monday 09:00 NZDT (08:00 NZST) + - cron: "0 20 * * 0" + workflow_dispatch: + +concurrency: + group: deps-weekly-refresh + cancel-in-progress: false + +permissions: + contents: read + +jobs: + refresh: + if: github.repository == 'mattrglobal/sample-apps' + runs-on: ubuntu-latest + timeout-minutes: 45 + env: + COREPACK_ENABLE_DOWNLOAD_PROMPT: "0" + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + YARN_NPM_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: 24 + registry-url: https://registry.npmjs.org + + - name: Refresh lockfiles + id: refresh + run: node scripts/deps/refresh.mjs --report "$RUNNER_TEMP/deps-report.md" + + - name: Open or update the PR + if: steps.refresh.outputs.changed == 'true' + uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1 + with: + # A PAT, not GITHUB_TOKEN, so the PR triggers the ci-apps checks. + token: ${{ secrets.GH_TOKEN }} + branch: deps/weekly-refresh + base: master + add-paths: | + **/package-lock.json + **/yarn.lock + commit-message: "build(deps): weekly in-range dependency refresh" + title: "build(deps): weekly in-range dependency refresh" + body-path: ${{ runner.temp }}/deps-report.md + sign-commits: true + delete-branch: true diff --git a/README.md b/README.md index 7c45fcd..b1d3f7a 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,17 @@ A collection of developer apps and API tooling that you can use to interface wit * [Credential templates](/credential-templates/README.md) * [Postman Collection](/postman/README.md) +## Dependency updates + +Every Monday morning (NZ time) two things run: + +* **Weekly refresh** (`.github/workflows/deps-weekly-refresh.yml`) re-resolves every JS app's lockfile to the newest versions its `package.json` ranges allow, transitive dependencies included, and opens or updates one PR from `deps/weekly-refresh`. It never edits `package.json`. Releases newer than 3 days are skipped. The PR body has a look-ahead: the direct dependencies still behind after merging (and whether Dependabot, a range change or a deliberate upgrade moves them), plus any advisories the refreshed lockfiles still carry. +* **Dependabot** (`.github/dependabot.yml`) only opens what the refresh can't do: major upgrades, grouped by dependency across apps, and security fixes. It also covers the Android Gradle apps, the Dockerfiles and GitHub Actions. + +Expo, React Native and MATTR SDK majors in the React Native tutorials are excluded from automation. Upgrade them together with the matching Learn tutorial using `npx expo install --fix`. If a refresh would move a package that has a `patches/` file, that app's lockfile is left alone and the PR says so. Regenerate the patch against the new version to unblock it. + +To run the refresh locally: `node scripts/deps/refresh.mjs --only `. + ---

Copyright © MATTR Limited. Some rights reserved.
“MATTR” is a trademark of MATTR Limited, registered in New Zealand and other countries.

diff --git a/scripts/deps/lockfile.mjs b/scripts/deps/lockfile.mjs new file mode 100644 index 0000000..94e1695 --- /dev/null +++ b/scripts/deps/lockfile.mjs @@ -0,0 +1,146 @@ +// Lockfile parsing for npm (lockfileVersion 3) and Yarn Berry (__metadata version 10). +// Each parser returns Map>. + +export function parseNpmLock(text) { + const lock = JSON.parse(text); + const versions = new Map(); + for (const [path, entry] of Object.entries(lock.packages ?? {})) { + if (!path || !entry.version || entry.link) continue; + const name = entry.name ?? path.slice(path.lastIndexOf("node_modules/") + "node_modules/".length); + add(versions, name, entry.version); + } + return versions; +} + +export function parseYarnLock(text) { + const versions = new Map(); + for (const block of yarnBlocks(text)) { + if (block.key === "__metadata" || !block.version) continue; + const name = descriptorName(block.descriptors[0]); + if (block.descriptors[0].includes("@workspace:")) continue; + add(versions, name, block.version); + } + return versions; +} + +// Resolved version of a direct dependency, matched on the exact range from package.json. +export function resolvedDirect(pm, text, name, range) { + if (pm === "npm") return JSON.parse(text).packages?.[`node_modules/${name}`]?.version; + const wanted = range.includes(":") ? `${name}@${range}` : `${name}@npm:${range}`; + for (const block of yarnBlocks(text)) { + if (block.descriptors.includes(wanted)) return block.version; + } + return undefined; +} + +// The direct dependencies whose dependency chains pull `name` in. +export function directAncestors(pm, text, name, directNames) { + const parents = pm === "npm" ? npmParents(text) : yarnParents(text); + const found = new Set(); + const seen = new Set(); + const queue = [name]; + while (queue.length) { + const current = queue.shift(); + if (seen.has(current)) continue; + seen.add(current); + if (directNames.has(current)) found.add(current); + queue.push(...(parents.get(current) ?? [])); + } + return [...found].sort(); +} + +function npmParents(text) { + const parents = new Map(); + for (const [path, entry] of Object.entries(JSON.parse(text).packages ?? {})) { + if (!path) continue; + const parent = entry.name ?? path.slice(path.lastIndexOf("node_modules/") + "node_modules/".length); + const deps = { ...entry.dependencies, ...entry.optionalDependencies, ...entry.peerDependencies }; + for (const dep of Object.keys(deps)) add(parents, dep, parent); + } + return parents; +} + +function yarnParents(text) { + const parents = new Map(); + for (const block of yarnBlocks(text)) { + const parent = descriptorName(block.descriptors[0]); + let inDeps = false; + for (const line of block.raw.split("\n")) { + if (/^ {2}\S/.test(line)) inDeps = /^ {2}(dependencies|peerDependencies):/.test(line); + else if (inDeps && /^ {4}\S/.test(line)) add(parents, line.trim().split(/"?:\s/)[0].replace(/^"/, ""), parent); + } + } + return parents; +} + +// Drop every Yarn lock entry except __metadata and the named packages, so the next +// `yarn install` re-resolves everything else to the newest version its range allows. +export function pruneYarnLock(text, keepNames) { + const lines = text.split("\n"); + const header = []; + for (const line of lines) { + if (line && !line.startsWith("#")) break; + header.push(line); + } + const kept = yarnBlocks(text) + .filter((b) => b.key === "__metadata" || keepNames.has(descriptorName(b.descriptors[0]))) + .map((b) => b.raw); + return `${header.join("\n")}\n${kept.join("\n\n")}\n`; +} + +export function diffVersions(before, after) { + const changed = []; + for (const name of new Set([...before.keys(), ...after.keys()])) { + const from = [...(before.get(name) ?? [])].sort(compareVersions); + const to = [...(after.get(name) ?? [])].sort(compareVersions); + if (from.join() !== to.join()) changed.push({ name, from, to }); + } + return changed.sort((a, b) => a.name.localeCompare(b.name)); +} + +export function compareVersions(a, b) { + const pa = parseVersion(a); + const pb = parseVersion(b); + for (let i = 0; i < 3; i++) if (pa[i] !== pb[i]) return pa[i] - pb[i]; + return 0; +} + +// Treat 0.x minor bumps as breaking, the way caret ranges do. +export function isMajorBump(from, to) { + const [fMaj, fMin] = parseVersion(from); + const [tMaj, tMin] = parseVersion(to); + return fMaj !== tMaj || (fMaj === 0 && fMin !== tMin); +} + +function parseVersion(v) { + return String(v) + .split("-")[0] + .split(".") + .map((n) => Number.parseInt(n, 10) || 0); +} + +function add(map, name, version) { + if (!map.has(name)) map.set(name, new Set()); + map.get(name).add(version); +} + +function descriptorName(descriptor) { + const at = descriptor.indexOf("@", 1); + return at === -1 ? descriptor : descriptor.slice(0, at); +} + +function yarnBlocks(text) { + const blocks = []; + for (const raw of text.split(/\n\s*\n/)) { + const lines = raw.split("\n").filter((l) => l && !l.startsWith("#")); + if (!lines.length || lines[0].startsWith(" ")) continue; + const key = lines[0].replace(/:$/, ""); + const descriptors = key + .replace(/^"|"$/g, "") + .split(/",\s*"|,\s*/) + .map((d) => d.trim()); + const version = lines.find((l) => l.startsWith(" version:"))?.split(":")[1].trim(); + blocks.push({ key, descriptors, version, raw: lines.join("\n") }); + } + return blocks; +} diff --git a/scripts/deps/lockfile.test.mjs b/scripts/deps/lockfile.test.mjs new file mode 100644 index 0000000..1669b62 --- /dev/null +++ b/scripts/deps/lockfile.test.mjs @@ -0,0 +1,114 @@ +import assert from "node:assert/strict"; +import { test } from "node:test"; +import { + compareVersions, + diffVersions, + directAncestors, + isMajorBump, + parseNpmLock, + parseYarnLock, + pruneYarnLock, + resolvedDirect, +} from "./lockfile.mjs"; + +const npmLock = JSON.stringify({ + lockfileVersion: 3, + packages: { + "": { name: "app", dependencies: { next: "16.3.8" } }, + "node_modules/next": { version: "16.3.8", dependencies: { "@next/env": "16.3.8" } }, + "node_modules/@next/env": { version: "16.3.8" }, + "node_modules/a/node_modules/qs": { version: "6.13.0" }, + "node_modules/qs": { version: "6.14.0" }, + "node_modules/local": { resolved: "../local", link: true }, + }, +}); + +const yarnLock = `# This file is generated by running "yarn install" inside your project. +# Manual changes might be lost - proceed with caution! + +__metadata: + version: 10 + cacheKey: 10c0 + +"@expo/cli@npm:55.0.32": + version: 55.0.32 + resolution: "@expo/cli@npm:55.0.32" + languageName: node + linkType: hard + +"expo@npm:^55": + version: 55.0.10 + resolution: "expo@npm:55.0.10" + dependencies: + "@expo/cli": "npm:55.0.32" + languageName: node + linkType: hard + +"glob@npm:^10.5.0, glob@npm:^10.2.2": + version: 10.5.0 + resolution: "glob@npm:10.5.0" + languageName: node + linkType: hard + +"app@workspace:.": + version: 0.0.0-use.local + resolution: "app@workspace:." + languageName: unknown + linkType: soft +`; + +test("parseNpmLock collects every installed copy and skips links and the root", () => { + const v = parseNpmLock(npmLock); + assert.deepEqual([...v.get("qs")].sort(), ["6.13.0", "6.14.0"]); + assert.deepEqual([...v.get("@next/env")], ["16.3.8"]); + assert.equal(v.has("local"), false); + assert.equal(v.has(""), false); +}); + +test("parseYarnLock reads scoped and multi-descriptor entries and skips the workspace", () => { + const v = parseYarnLock(yarnLock); + assert.deepEqual([...v.get("@expo/cli")], ["55.0.32"]); + assert.deepEqual([...v.get("glob")], ["10.5.0"]); + assert.equal(v.has("app"), false); + assert.equal(v.has("__metadata"), false); +}); + +test("resolvedDirect matches the exact package.json range", () => { + assert.equal(resolvedDirect("npm", npmLock, "next", "16.3.8"), "16.3.8"); + assert.equal(resolvedDirect("yarn", yarnLock, "expo", "^55"), "55.0.10"); + assert.equal(resolvedDirect("yarn", yarnLock, "glob", "^10.2.2"), "10.5.0"); + assert.equal(resolvedDirect("yarn", yarnLock, "glob", "^9"), undefined); +}); + +test("pruneYarnLock keeps the header, metadata and held packages only", () => { + const pruned = pruneYarnLock(yarnLock, new Set(["@expo/cli"])); + assert.match(pruned, /^# This file is generated/); + assert.match(pruned, /__metadata:\n {2}version: 10/); + assert.match(pruned, /"@expo\/cli@npm:55.0.32":/); + assert.doesNotMatch(pruned, /expo@npm:\^55|glob@npm|workspace/); + assert.deepEqual([...parseYarnLock(pruned).keys()], ["@expo/cli"]); +}); + +test("diffVersions reports changed, added and removed packages", () => { + const before = new Map([["a", new Set(["1.0.0"])], ["b", new Set(["2.0.0"])]]); + const after = new Map([["a", new Set(["1.1.0"])], ["c", new Set(["3.0.0"])]]); + assert.deepEqual(diffVersions(before, after), [ + { name: "a", from: ["1.0.0"], to: ["1.1.0"] }, + { name: "b", from: ["2.0.0"], to: [] }, + { name: "c", from: [], to: ["3.0.0"] }, + ]); +}); + +test("version helpers", () => { + assert.ok(compareVersions("10.0.0", "9.9.9") > 0); + assert.ok(compareVersions("1.2.3", "1.2.10") < 0); + assert.equal(isMajorBump("9.0.2", "10.0.0"), true); + assert.equal(isMajorBump("0.3.1", "0.4.0"), true); + assert.equal(isMajorBump("16.1.5", "16.3.8"), false); +}); + +test("directAncestors walks the dependency graph up to direct dependencies", () => { + assert.deepEqual(directAncestors("npm", npmLock, "@next/env", new Set(["next"])), ["next"]); + assert.deepEqual(directAncestors("yarn", yarnLock, "@expo/cli", new Set(["expo", "glob"])), ["expo"]); + assert.deepEqual(directAncestors("yarn", yarnLock, "glob", new Set(["expo"])), []); +}); diff --git a/scripts/deps/refresh.mjs b/scripts/deps/refresh.mjs new file mode 100644 index 0000000..805273d --- /dev/null +++ b/scripts/deps/refresh.mjs @@ -0,0 +1,272 @@ +#!/usr/bin/env node +// Weekly in-range dependency refresh across every JS app in the repo. +// +// Usage: node scripts/deps/refresh.mjs [--report ] [--only [,]] +// +// For each app: re-resolve the whole lockfile to the newest versions its package.json +// ranges allow (package.json is never changed), then report what changed and look ahead +// at what is still pending: direct dependencies whose latest release is outside their +// range, and advisories the refreshed lockfile still carries. + +import { execFileSync, spawnSync } from "node:child_process"; +import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; +import { + compareVersions, + diffVersions, + directAncestors, + isMajorBump, + parseNpmLock, + parseYarnLock, + pruneYarnLock, + resolvedDirect, +} from "./lockfile.mjs"; + +// Matches the 3-day npmMinimalAgeGate in each app's .yarnrc.yml. +const MIN_AGE_DAYS = 3; + +// Upgraded deliberately alongside the Learn tutorials, never by automation. +const MANUAL_UPGRADE = [/^expo$/, /^expo-/, /^@expo\//, /^react-native$/, /^react$/, /^react-dom$/, /^@mattrglobal\//]; + +const args = parseArgs(process.argv.slice(2)); +const root = execFileSync("git", ["rev-parse", "--show-toplevel"], { encoding: "utf8" }).trim(); +const latestCache = new Map(); + +const apps = discoverApps().filter((a) => !args.only || args.only.includes(a.dir)); +const results = []; +for (const app of apps) { + console.log(`\n== ${app.dir} (${app.pm})`); + results.push(await processApp(app)); +} + +const report = renderReport(results); +writeFileSync(args.report, report); +const changed = results.some((r) => r.changes.length); +if (process.env.GITHUB_OUTPUT) writeFileSync(process.env.GITHUB_OUTPUT, `changed=${changed}\n`, { flag: "a" }); +console.log(`\nReport written to ${args.report} (lockfiles changed: ${changed})`); + +async function processApp(app) { + const cwd = join(root, app.dir); + const lockPath = join(cwd, app.lockfile); + const pkgPath = join(cwd, "package.json"); + const pkgBefore = readFileSync(pkgPath, "utf8"); + const lockBefore = readFileSync(lockPath, "utf8"); + const pkg = JSON.parse(pkgBefore); + const held = patchedPackages(cwd); + const result = { ...app, changes: [], directChanges: [], pending: [], advisories: [], notes: [] }; + + const refresh = app.pm === "npm" ? refreshNpm(cwd) : refreshYarn(cwd, lockPath, lockBefore, held); + if (readFileSync(pkgPath, "utf8") !== pkgBefore) writeFileSync(pkgPath, pkgBefore); + if (!refresh.ok) { + writeFileSync(lockPath, lockBefore); + result.notes.push(`Refresh failed, lockfile left unchanged:\n\n\`\`\`\n${tail(refresh.output)}\n\`\`\``); + } + + let lockAfter = readFileSync(lockPath, "utf8"); + const parse = app.pm === "npm" ? parseNpmLock : parseYarnLock; + const moved = [...held].filter(([name, version]) => !parse(lockAfter).get(name)?.has(version)); + if (moved.length) { + writeFileSync(lockPath, lockBefore); + lockAfter = lockBefore; + const list = moved.map(([n, v]) => `\`${n}@${v}\``).join(", "); + result.notes.push( + `Refresh reverted: it would move ${list}, which \`patches/\` pins. Regenerate the patch against the new version to unblock this app.`, + ); + } + + const direct = { ...pkg.dependencies, ...pkg.devDependencies }; + result.changes = diffVersions(parse(lockBefore), parse(lockAfter)); + result.directChanges = result.changes.filter((c) => c.name in direct); + result.pending = await outOfRange(app, lockAfter, direct); + const advisories = app.pm === "npm" ? auditNpm(cwd) : auditYarn(cwd); + const directNames = new Set(Object.keys(direct)); + result.advisories = advisories.map((a) => ({ + ...a, + fix: describeFix(a.fix, a.fix?.name && resolvedDirect(app.pm, lockAfter, a.fix.name, direct[a.fix.name] ?? "")), + via: directNames.has(a.name) ? "direct" : directAncestors(app.pm, lockAfter, a.name, directNames).join(", ") || "unknown", + })); + return result; +} + +function refreshNpm(cwd) { + const before = new Date(Date.now() - MIN_AGE_DAYS * 86_400_000).toISOString(); + return run("npm", ["update", "--package-lock-only", "--ignore-scripts", "--no-audit", "--no-fund", `--before=${before}`], cwd); +} + +function refreshYarn(cwd, lockPath, lockText, held) { + writeFileSync(lockPath, pruneYarnLock(lockText, new Set(held.keys()))); + return run("corepack", ["yarn", "install", "--mode=update-lockfile"], cwd, { YARN_ENABLE_IMMUTABLE_INSTALLS: "false" }); +} + +// patch-package patches are named +.patch, with "/" in scoped names as "+". +function patchedPackages(cwd) { + const dir = join(cwd, "patches"); + const held = new Map(); + if (!existsSync(dir)) return held; + for (const file of readdirSync(dir)) { + const match = file.match(/^(.+)\+([^+]+)\.patch$/); + if (match) held.set(match[1].replace("+", "/"), match[2]); + } + return held; +} + +async function outOfRange(app, lockText, direct) { + const pending = []; + for (const [name, range] of Object.entries(direct)) { + const current = resolvedDirect(app.pm, lockText, name, range); + const latest = await latestVersion(name); + if (!current || !latest || compareVersions(latest, current) <= 0) continue; + const manual = app.reactNative && MANUAL_UPGRADE.some((re) => re.test(name)); + const owner = manual + ? "Manual (Expo or MATTR SDK upgrade)" + : isMajorBump(current, latest) + ? "Dependabot major PR" + : isPinned(range, current, latest) + ? "Range is pinned, widen by hand" + : `Next refresh (released under ${MIN_AGE_DAYS} days ago)`; + pending.push({ name, range, current, latest, owner }); + } + return pending; +} + +// Exact pins and tilde ranges that exclude a newer minor. Caret ranges always allow non-major bumps. +function isPinned(range, current, latest) { + if (/^\d+\.\d+\.\d+$/.test(range)) return true; + return range.startsWith("~") && current.split(".")[1] !== latest.split(".")[1]; +} + +async function latestVersion(name) { + if (!latestCache.has(name)) { + const { ok, stdout } = run("npm", ["view", name, "dist-tags.latest", "--json"], root, {}, false); + latestCache.set(name, ok ? JSON.parse(stdout.trim() || "null") : undefined); + } + return latestCache.get(name); +} + +function auditNpm(cwd) { + const { output } = run("npm", ["audit", "--package-lock-only", "--json"], cwd); + let vulns; + try { + vulns = JSON.parse(output).vulnerabilities ?? {}; + } catch { + return [{ name: "(audit failed)", severity: "unknown", fix: tail(output, 1) }]; + } + return Object.values(vulns) + .filter((v) => v.via.some((x) => typeof x === "object")) + .map((v) => ({ name: v.name, severity: v.severity, fix: v.fixAvailable })); +} + +function describeFix(fix, current) { + if (typeof fix === "string") return fix; + if (fix === true) return "fixable in range"; + if (!fix) return "none via `npm audit fix`"; + if (current && compareVersions(fix.version, current) < 0) return `no upstream fix yet (npm would downgrade ${fix.name} to ${fix.version})`; + return `needs ${fix.name}@${fix.version}${fix.isSemVerMajor ? " (major)" : ""}`; +} + +function auditYarn(cwd) { + const { output } = run("corepack", ["yarn", "npm", "audit", "--all", "--recursive", "--json"], cwd); + const advisories = []; + for (const line of output.split("\n")) { + if (!line.startsWith("{")) continue; + try { + const { value, children } = JSON.parse(line); + advisories.push({ + name: value, + severity: children?.Severity ?? "unknown", + fix: children?.["Vulnerable Versions"] ? `vulnerable ${children["Vulnerable Versions"]}` : "", + }); + } catch {} + } + if (!advisories.length && /YN0001|Error/.test(output)) { + return [{ name: "(audit failed)", severity: "unknown", fix: tail(output, 1) }]; + } + return advisories; +} + +function renderReport(results) { + const out = [ + "Weekly in-range refresh: every lockfile is re-resolved to the newest versions its `package.json` ranges allow. No `package.json` changes.", + "", + "| App | Direct | Transitive | Still pending after merge | Open advisories |", + "| --- | --- | --- | --- | --- |", + ]; + for (const r of results) { + const transitive = r.changes.length - r.directChanges.length; + const flag = r.notes.length ? " (see notes)" : ""; + out.push(`| \`${r.dir}\`${flag} | ${r.directChanges.length} | ${transitive} | ${r.pending.length} | ${r.advisories.length} |`); + } + out.push("", "## Look-ahead: what still needs a human or a Dependabot PR", ""); + const pending = results.flatMap((r) => r.pending.map((p) => ({ ...p, dir: r.dir }))); + if (pending.length) { + out.push("| App | Package | Range | Locked | Latest | Who moves it |", "| --- | --- | --- | --- | --- | --- |"); + for (const p of pending) out.push(`| \`${p.dir}\` | \`${p.name}\` | \`${p.range}\` | ${p.current} | ${p.latest} | ${p.owner} |`); + } else { + out.push("Every direct dependency is on its latest release."); + } + out.push("", "### Advisories the refreshed lockfiles still carry", ""); + const advisories = results.flatMap((r) => r.advisories.map((a) => ({ ...a, dir: r.dir }))); + if (advisories.length) { + out.push("| App | Package | Severity | Pulled in by | Fix |", "| --- | --- | --- | --- | --- |"); + for (const a of advisories) out.push(`| \`${a.dir}\` | \`${a.name}\` | ${a.severity} | ${esc(a.via)} | ${esc(a.fix)} |`); + } else { + out.push("None."); + } + out.push("", "## Changes per app", ""); + for (const r of results) { + if (!r.changes.length && !r.notes.length) continue; + out.push(`### \`${r.dir}\``, "", ...r.notes.flatMap((n) => [`> ${n.replaceAll("\n", "\n> ")}`, ""])); + for (const c of r.directChanges) out.push(`- \`${c.name}\` ${c.from.join(", ") || "(new)"} → ${c.to.join(", ") || "(removed)"}`); + const transitive = r.changes.filter((c) => !r.directChanges.includes(c)); + if (transitive.length) { + out.push("", `
${transitive.length} transitive`, ""); + for (const c of transitive) out.push(`- \`${c.name}\` ${c.from.join(", ") || "(new)"} → ${c.to.join(", ") || "(removed)"}`); + out.push("", "
"); + } + out.push(""); + } + return `${out.join("\n")}\n`; +} + +function discoverApps() { + const files = execFileSync("git", ["ls-files", "*package.json"], { cwd: root, encoding: "utf8" }).split("\n"); + return files + .filter((f) => f.endsWith("package.json")) + .map((f) => f.slice(0, -"/package.json".length)) + .filter((dir) => dir && !dir.includes("node_modules")) + .flatMap((dir) => { + const lockfile = ["package-lock.json", "yarn.lock"].find((l) => existsSync(join(root, dir, l))); + if (!lockfile) return []; + const pm = lockfile === "yarn.lock" ? "yarn" : "npm"; + return [{ dir, lockfile, pm, reactNative: dir.startsWith("react-native-") }]; + }) + .sort((a, b) => a.dir.localeCompare(b.dir)); +} + +function run(cmd, cmdArgs, cwd, env = {}, log = true) { + if (log) console.log(`$ ${cmd} ${cmdArgs.join(" ")}`); + const res = spawnSync(cmd, cmdArgs, { cwd, encoding: "utf8", maxBuffer: 256 * 1024 * 1024, env: { ...process.env, ...env } }); + return { ok: res.status === 0, output: `${res.stdout ?? ""}${res.stderr ?? ""}`, stdout: res.stdout ?? "" }; +} + +function esc(cell) { + return String(cell).replaceAll("|", "\\|"); +} + +function tail(text, lines = 15) { + return text + .trim() + .split("\n") + .filter((l) => !/^\s+at /.test(l)) + .slice(-lines) + .join("\n"); +} + +function parseArgs(argv) { + const parsed = { report: "deps-report.md", only: undefined }; + for (let i = 0; i < argv.length; i++) { + if (argv[i] === "--report") parsed.report = argv[++i]; + else if (argv[i] === "--only") parsed.only = argv[++i].split(","); + } + return parsed; +} From 57a4f09b5d99dbcf5b2795834c169ac8894635d6 Mon Sep 17 00:00:00 2001 From: Avner Matan <137777701+avner-m@users.noreply.github.com> Date: Tue, 13 Oct 2026 15:43:31 +1300 Subject: [PATCH 2/3] build(ci): drop the look-ahead report and registry tokens from the weekly refresh --- .github/dependabot.yml | 8 - .github/workflows/ci-apps.yml | 15 +- .github/workflows/deps-weekly-refresh.yml | 20 ++- README.md | 4 +- interaction-hook-app/postcss.config.js | 6 - interaction-hook-app/tailwind.config.ts | 20 --- scripts/deps/lockfile.mjs | 63 +------ scripts/deps/lockfile.test.mjs | 34 +--- scripts/deps/refresh.mjs | 199 ++++------------------ 9 files changed, 66 insertions(+), 303 deletions(-) delete mode 100644 interaction-hook-app/postcss.config.js delete mode 100644 interaction-hook-app/tailwind.config.ts diff --git a/.github/dependabot.yml b/.github/dependabot.yml index f7c451f..f0c2aa2 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -4,12 +4,6 @@ version: 2 # refresh workflow (deps-weekly-refresh.yml), which updates every lockfile in one PR. # Dependabot only opens what the refresh can't do: majors, and security fixes. -registries: - npm-mattr: - type: npm-registry - url: https://registry.npmjs.org - token: ${{ secrets.NPM_TOKEN }} - updates: # Node and Next.js apps - package-ecosystem: npm @@ -66,8 +60,6 @@ updates: - "/react-native-mdocs-holder-tutorial/react-native-mdocs-holder-tutorial-starter" - "/react-native-remote-verification-tutorial/react-native-remote-verification-tutorial-complete" - "/react-native-remote-verification-tutorial/react-native-remote-verification-tutorial-starter" - registries: - - npm-mattr schedule: interval: weekly day: monday diff --git a/.github/workflows/ci-apps.yml b/.github/workflows/ci-apps.yml index 152a105..ab969ac 100644 --- a/.github/workflows/ci-apps.yml +++ b/.github/workflows/ci-apps.yml @@ -2,11 +2,14 @@ name: CI apps # Installs each changed JS app from its lockfile, then builds the Next.js apps and # type-checks the complete React Native apps. The native Android and iOS apps aren't -# built: their MATTR SDKs are added locally and aren't available in CI. +# built: their MATTR SDKs are added locally and aren't available in CI. The React Native +# apps install a private MATTR SDK, so they're only checked when an NPM_TOKEN secret exists. on: pull_request: branches: [master] + # Started by deps-weekly-refresh.yml for its PR branch. + workflow_dispatch: permissions: contents: read @@ -27,6 +30,7 @@ jobs: env: BASE_SHA: ${{ github.event.pull_request.base.sha }} run: | + BASE_SHA=${BASE_SHA:-$(git merge-base origin/master HEAD)} apps=$(git ls-files '*package-lock.json' '*yarn.lock' | xargs -n1 dirname | while read -r dir; do if ! git diff --quiet "$BASE_SHA"...HEAD -- "$dir"; then echo "$dir"; fi done | jq -R . | jq -sc .) @@ -47,8 +51,7 @@ jobs: working-directory: ${{ matrix.app }} env: COREPACK_ENABLE_DOWNLOAD_PROMPT: "0" - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - YARN_NPM_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} NEXT_TELEMETRY_DISABLED: "1" steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -58,16 +61,16 @@ jobs: - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: 24 - registry-url: https://registry.npmjs.org - name: Skip without registry access id: access - if: startsWith(matrix.app, 'react-native-') && env.YARN_NPM_AUTH_TOKEN == '' - run: echo "::notice::NPM_TOKEN isn't available (fork PR?), skipping ${{ matrix.app }}" + if: startsWith(matrix.app, 'react-native-') && env.NPM_TOKEN == '' + run: echo "::notice::No NPM_TOKEN secret for the private MATTR SDK, skipping ${{ matrix.app }}" - name: Install if: steps.access.conclusion == 'skipped' run: | + if [ -n "$NPM_TOKEN" ]; then export YARN_NPM_AUTH_TOKEN="$NPM_TOKEN"; fi if [ -f yarn.lock ]; then corepack yarn install --immutable --mode=skip-build else diff --git a/.github/workflows/deps-weekly-refresh.yml b/.github/workflows/deps-weekly-refresh.yml index aa24d65..5f38d20 100644 --- a/.github/workflows/deps-weekly-refresh.yml +++ b/.github/workflows/deps-weekly-refresh.yml @@ -1,7 +1,7 @@ name: Weekly dependency refresh # Re-resolves every app's lockfile to the newest versions its package.json ranges allow, -# and opens (or updates) a single PR whose body looks ahead at what is still pending. +# and opens (or updates) a single PR listing what changed. on: schedule: @@ -14,7 +14,9 @@ concurrency: cancel-in-progress: false permissions: - contents: read + contents: write + pull-requests: write + actions: write jobs: refresh: @@ -23,8 +25,6 @@ jobs: timeout-minutes: 45 env: COREPACK_ENABLE_DOWNLOAD_PROMPT: "0" - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - YARN_NPM_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: @@ -33,18 +33,16 @@ jobs: - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: 24 - registry-url: https://registry.npmjs.org - name: Refresh lockfiles id: refresh run: node scripts/deps/refresh.mjs --report "$RUNNER_TEMP/deps-report.md" - name: Open or update the PR + id: pr if: steps.refresh.outputs.changed == 'true' uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1 with: - # A PAT, not GITHUB_TOKEN, so the PR triggers the ci-apps checks. - token: ${{ secrets.GH_TOKEN }} branch: deps/weekly-refresh base: master add-paths: | @@ -55,3 +53,11 @@ jobs: body-path: ${{ runner.temp }}/deps-report.md sign-commits: true delete-branch: true + + # Pushes made with GITHUB_TOKEN don't trigger pull_request workflows, so start the + # app checks on the PR branch directly. They report on the PR's head commit. + - name: Run app checks on the PR + if: steps.pr.outputs.pull-request-operation == 'created' || steps.pr.outputs.pull-request-operation == 'updated' + env: + GH_TOKEN: ${{ github.token }} + run: gh workflow run ci-apps.yml --repo "$GITHUB_REPOSITORY" --ref deps/weekly-refresh diff --git a/README.md b/README.md index b1d3f7a..93826ba 100644 --- a/README.md +++ b/README.md @@ -18,10 +18,10 @@ A collection of developer apps and API tooling that you can use to interface wit Every Monday morning (NZ time) two things run: -* **Weekly refresh** (`.github/workflows/deps-weekly-refresh.yml`) re-resolves every JS app's lockfile to the newest versions its `package.json` ranges allow, transitive dependencies included, and opens or updates one PR from `deps/weekly-refresh`. It never edits `package.json`. Releases newer than 3 days are skipped. The PR body has a look-ahead: the direct dependencies still behind after merging (and whether Dependabot, a range change or a deliberate upgrade moves them), plus any advisories the refreshed lockfiles still carry. +* **Weekly refresh** (`.github/workflows/deps-weekly-refresh.yml`) re-resolves every JS app's lockfile to the newest versions its `package.json` ranges allow, transitive dependencies included, and opens or updates one PR from `deps/weekly-refresh`. It never edits `package.json`, and skips releases newer than 3 days. Merging it clears the chain of transitive bumps that would otherwise arrive one PR at a time. * **Dependabot** (`.github/dependabot.yml`) only opens what the refresh can't do: major upgrades, grouped by dependency across apps, and security fixes. It also covers the Android Gradle apps, the Dockerfiles and GitHub Actions. -Expo, React Native and MATTR SDK majors in the React Native tutorials are excluded from automation. Upgrade them together with the matching Learn tutorial using `npx expo install --fix`. If a refresh would move a package that has a `patches/` file, that app's lockfile is left alone and the PR says so. Regenerate the patch against the new version to unblock it. +Expo, React Native and MATTR SDK majors in the React Native tutorials are excluded from automation, and the refresh leaves the MATTR SDK lock entries as they are. Upgrade them together with the matching Learn tutorial using `npx expo install --fix`. If a refresh would move a package that has a `patches/` file, that app's lockfile is left alone and the PR says so. Regenerate the patch against the new version to unblock it. To run the refresh locally: `node scripts/deps/refresh.mjs --only `. diff --git a/interaction-hook-app/postcss.config.js b/interaction-hook-app/postcss.config.js deleted file mode 100644 index 33ad091..0000000 --- a/interaction-hook-app/postcss.config.js +++ /dev/null @@ -1,6 +0,0 @@ -module.exports = { - plugins: { - tailwindcss: {}, - autoprefixer: {}, - }, -} diff --git a/interaction-hook-app/tailwind.config.ts b/interaction-hook-app/tailwind.config.ts deleted file mode 100644 index 1af3b8f..0000000 --- a/interaction-hook-app/tailwind.config.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { Config } from 'tailwindcss' - -const config: Config = { - content: [ - './src/pages/**/*.{js,ts,jsx,tsx,mdx}', - './src/components/**/*.{js,ts,jsx,tsx,mdx}', - './src/app/**/*.{js,ts,jsx,tsx,mdx}', - ], - theme: { - extend: { - backgroundImage: { - 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))', - 'gradient-conic': - 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))', - }, - }, - }, - plugins: [], -} -export default config diff --git a/scripts/deps/lockfile.mjs b/scripts/deps/lockfile.mjs index 94e1695..b01f464 100644 --- a/scripts/deps/lockfile.mjs +++ b/scripts/deps/lockfile.mjs @@ -23,59 +23,9 @@ export function parseYarnLock(text) { return versions; } -// Resolved version of a direct dependency, matched on the exact range from package.json. -export function resolvedDirect(pm, text, name, range) { - if (pm === "npm") return JSON.parse(text).packages?.[`node_modules/${name}`]?.version; - const wanted = range.includes(":") ? `${name}@${range}` : `${name}@npm:${range}`; - for (const block of yarnBlocks(text)) { - if (block.descriptors.includes(wanted)) return block.version; - } - return undefined; -} - -// The direct dependencies whose dependency chains pull `name` in. -export function directAncestors(pm, text, name, directNames) { - const parents = pm === "npm" ? npmParents(text) : yarnParents(text); - const found = new Set(); - const seen = new Set(); - const queue = [name]; - while (queue.length) { - const current = queue.shift(); - if (seen.has(current)) continue; - seen.add(current); - if (directNames.has(current)) found.add(current); - queue.push(...(parents.get(current) ?? [])); - } - return [...found].sort(); -} - -function npmParents(text) { - const parents = new Map(); - for (const [path, entry] of Object.entries(JSON.parse(text).packages ?? {})) { - if (!path) continue; - const parent = entry.name ?? path.slice(path.lastIndexOf("node_modules/") + "node_modules/".length); - const deps = { ...entry.dependencies, ...entry.optionalDependencies, ...entry.peerDependencies }; - for (const dep of Object.keys(deps)) add(parents, dep, parent); - } - return parents; -} - -function yarnParents(text) { - const parents = new Map(); - for (const block of yarnBlocks(text)) { - const parent = descriptorName(block.descriptors[0]); - let inDeps = false; - for (const line of block.raw.split("\n")) { - if (/^ {2}\S/.test(line)) inDeps = /^ {2}(dependencies|peerDependencies):/.test(line); - else if (inDeps && /^ {4}\S/.test(line)) add(parents, line.trim().split(/"?:\s/)[0].replace(/^"/, ""), parent); - } - } - return parents; -} - // Drop every Yarn lock entry except __metadata and the named packages, so the next // `yarn install` re-resolves everything else to the newest version its range allows. -export function pruneYarnLock(text, keepNames) { +export function pruneYarnLock(text, keep) { const lines = text.split("\n"); const header = []; for (const line of lines) { @@ -83,7 +33,7 @@ export function pruneYarnLock(text, keepNames) { header.push(line); } const kept = yarnBlocks(text) - .filter((b) => b.key === "__metadata" || keepNames.has(descriptorName(b.descriptors[0]))) + .filter((b) => b.key === "__metadata" || keep(descriptorName(b.descriptors[0]))) .map((b) => b.raw); return `${header.join("\n")}\n${kept.join("\n\n")}\n`; } @@ -98,20 +48,13 @@ export function diffVersions(before, after) { return changed.sort((a, b) => a.name.localeCompare(b.name)); } -export function compareVersions(a, b) { +function compareVersions(a, b) { const pa = parseVersion(a); const pb = parseVersion(b); for (let i = 0; i < 3; i++) if (pa[i] !== pb[i]) return pa[i] - pb[i]; return 0; } -// Treat 0.x minor bumps as breaking, the way caret ranges do. -export function isMajorBump(from, to) { - const [fMaj, fMin] = parseVersion(from); - const [tMaj, tMin] = parseVersion(to); - return fMaj !== tMaj || (fMaj === 0 && fMin !== tMin); -} - function parseVersion(v) { return String(v) .split("-")[0] diff --git a/scripts/deps/lockfile.test.mjs b/scripts/deps/lockfile.test.mjs index 1669b62..b6be5d8 100644 --- a/scripts/deps/lockfile.test.mjs +++ b/scripts/deps/lockfile.test.mjs @@ -1,15 +1,6 @@ import assert from "node:assert/strict"; import { test } from "node:test"; -import { - compareVersions, - diffVersions, - directAncestors, - isMajorBump, - parseNpmLock, - parseYarnLock, - pruneYarnLock, - resolvedDirect, -} from "./lockfile.mjs"; +import { diffVersions, parseNpmLock, parseYarnLock, pruneYarnLock } from "./lockfile.mjs"; const npmLock = JSON.stringify({ lockfileVersion: 3, @@ -73,15 +64,8 @@ test("parseYarnLock reads scoped and multi-descriptor entries and skips the work assert.equal(v.has("__metadata"), false); }); -test("resolvedDirect matches the exact package.json range", () => { - assert.equal(resolvedDirect("npm", npmLock, "next", "16.3.8"), "16.3.8"); - assert.equal(resolvedDirect("yarn", yarnLock, "expo", "^55"), "55.0.10"); - assert.equal(resolvedDirect("yarn", yarnLock, "glob", "^10.2.2"), "10.5.0"); - assert.equal(resolvedDirect("yarn", yarnLock, "glob", "^9"), undefined); -}); - test("pruneYarnLock keeps the header, metadata and held packages only", () => { - const pruned = pruneYarnLock(yarnLock, new Set(["@expo/cli"])); + const pruned = pruneYarnLock(yarnLock, (name) => name === "@expo/cli"); assert.match(pruned, /^# This file is generated/); assert.match(pruned, /__metadata:\n {2}version: 10/); assert.match(pruned, /"@expo\/cli@npm:55.0.32":/); @@ -98,17 +82,3 @@ test("diffVersions reports changed, added and removed packages", () => { { name: "c", from: [], to: ["3.0.0"] }, ]); }); - -test("version helpers", () => { - assert.ok(compareVersions("10.0.0", "9.9.9") > 0); - assert.ok(compareVersions("1.2.3", "1.2.10") < 0); - assert.equal(isMajorBump("9.0.2", "10.0.0"), true); - assert.equal(isMajorBump("0.3.1", "0.4.0"), true); - assert.equal(isMajorBump("16.1.5", "16.3.8"), false); -}); - -test("directAncestors walks the dependency graph up to direct dependencies", () => { - assert.deepEqual(directAncestors("npm", npmLock, "@next/env", new Set(["next"])), ["next"]); - assert.deepEqual(directAncestors("yarn", yarnLock, "@expo/cli", new Set(["expo", "glob"])), ["expo"]); - assert.deepEqual(directAncestors("yarn", yarnLock, "glob", new Set(["expo"])), []); -}); diff --git a/scripts/deps/refresh.mjs b/scripts/deps/refresh.mjs index 805273d..382775e 100644 --- a/scripts/deps/refresh.mjs +++ b/scripts/deps/refresh.mjs @@ -3,89 +3,60 @@ // // Usage: node scripts/deps/refresh.mjs [--report ] [--only [,]] // -// For each app: re-resolve the whole lockfile to the newest versions its package.json -// ranges allow (package.json is never changed), then report what changed and look ahead -// at what is still pending: direct dependencies whose latest release is outside their -// range, and advisories the refreshed lockfile still carries. +// For each app, re-resolve the whole lockfile (transitive dependencies included) to the +// newest versions its package.json ranges allow. package.json is never changed. import { execFileSync, spawnSync } from "node:child_process"; import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs"; import { join } from "node:path"; -import { - compareVersions, - diffVersions, - directAncestors, - isMajorBump, - parseNpmLock, - parseYarnLock, - pruneYarnLock, - resolvedDirect, -} from "./lockfile.mjs"; +import { diffVersions, parseNpmLock, parseYarnLock, pruneYarnLock } from "./lockfile.mjs"; -// Matches the 3-day npmMinimalAgeGate in each app's .yarnrc.yml. +// Matches the 3-day npmMinimalAgeGate in each Yarn app's .yarnrc.yml. const MIN_AGE_DAYS = 3; -// Upgraded deliberately alongside the Learn tutorials, never by automation. -const MANUAL_UPGRADE = [/^expo$/, /^expo-/, /^@expo\//, /^react-native$/, /^react$/, /^react-dom$/, /^@mattrglobal\//]; - const args = parseArgs(process.argv.slice(2)); const root = execFileSync("git", ["rev-parse", "--show-toplevel"], { encoding: "utf8" }).trim(); -const latestCache = new Map(); -const apps = discoverApps().filter((a) => !args.only || args.only.includes(a.dir)); -const results = []; -for (const app of apps) { - console.log(`\n== ${app.dir} (${app.pm})`); - results.push(await processApp(app)); -} +const results = discoverApps() + .filter((a) => !args.only || args.only.includes(a.dir)) + .map((app) => { + console.log(`\n== ${app.dir} (${app.pm})`); + return processApp(app); + }); -const report = renderReport(results); -writeFileSync(args.report, report); +writeFileSync(args.report, renderReport(results)); const changed = results.some((r) => r.changes.length); if (process.env.GITHUB_OUTPUT) writeFileSync(process.env.GITHUB_OUTPUT, `changed=${changed}\n`, { flag: "a" }); console.log(`\nReport written to ${args.report} (lockfiles changed: ${changed})`); -async function processApp(app) { +function processApp(app) { const cwd = join(root, app.dir); const lockPath = join(cwd, app.lockfile); const pkgPath = join(cwd, "package.json"); const pkgBefore = readFileSync(pkgPath, "utf8"); const lockBefore = readFileSync(lockPath, "utf8"); - const pkg = JSON.parse(pkgBefore); + const parse = app.pm === "npm" ? parseNpmLock : parseYarnLock; const held = patchedPackages(cwd); - const result = { ...app, changes: [], directChanges: [], pending: [], advisories: [], notes: [] }; + const notes = []; const refresh = app.pm === "npm" ? refreshNpm(cwd) : refreshYarn(cwd, lockPath, lockBefore, held); if (readFileSync(pkgPath, "utf8") !== pkgBefore) writeFileSync(pkgPath, pkgBefore); if (!refresh.ok) { writeFileSync(lockPath, lockBefore); - result.notes.push(`Refresh failed, lockfile left unchanged:\n\n\`\`\`\n${tail(refresh.output)}\n\`\`\``); + notes.push(`Refresh failed, lockfile left unchanged:\n\n\`\`\`\n${tail(refresh.output)}\n\`\`\``); } - let lockAfter = readFileSync(lockPath, "utf8"); - const parse = app.pm === "npm" ? parseNpmLock : parseYarnLock; - const moved = [...held].filter(([name, version]) => !parse(lockAfter).get(name)?.has(version)); + const moved = [...held].filter(([name, version]) => !parse(readFileSync(lockPath, "utf8")).get(name)?.has(version)); if (moved.length) { writeFileSync(lockPath, lockBefore); - lockAfter = lockBefore; const list = moved.map(([n, v]) => `\`${n}@${v}\``).join(", "); - result.notes.push( - `Refresh reverted: it would move ${list}, which \`patches/\` pins. Regenerate the patch against the new version to unblock this app.`, - ); + notes.push(`Refresh reverted: it would move ${list}, which \`patches/\` pins. Regenerate the patch against the new version to unblock this app.`); } + const pkg = JSON.parse(pkgBefore); const direct = { ...pkg.dependencies, ...pkg.devDependencies }; - result.changes = diffVersions(parse(lockBefore), parse(lockAfter)); - result.directChanges = result.changes.filter((c) => c.name in direct); - result.pending = await outOfRange(app, lockAfter, direct); - const advisories = app.pm === "npm" ? auditNpm(cwd) : auditYarn(cwd); - const directNames = new Set(Object.keys(direct)); - result.advisories = advisories.map((a) => ({ - ...a, - fix: describeFix(a.fix, a.fix?.name && resolvedDirect(app.pm, lockAfter, a.fix.name, direct[a.fix.name] ?? "")), - via: directNames.has(a.name) ? "direct" : directAncestors(app.pm, lockAfter, a.name, directNames).join(", ") || "unknown", - })); - return result; + const changes = diffVersions(parse(lockBefore), parse(readFileSync(lockPath, "utf8"))); + return { ...app, changes, directChanges: changes.filter((c) => c.name in direct), notes }; } function refreshNpm(cwd) { @@ -93,8 +64,11 @@ function refreshNpm(cwd) { return run("npm", ["update", "--package-lock-only", "--ignore-scripts", "--no-audit", "--no-fund", `--before=${before}`], cwd); } +// Lock entries for MATTR's private SDK packages are kept as they are, so the refresh +// needs no registry credentials. SDK upgrades are made deliberately with the tutorials. function refreshYarn(cwd, lockPath, lockText, held) { - writeFileSync(lockPath, pruneYarnLock(lockText, new Set(held.keys()))); + const keep = (name) => held.has(name) || name.startsWith("@mattrglobal/"); + writeFileSync(lockPath, pruneYarnLock(lockText, keep)); return run("corepack", ["yarn", "install", "--mode=update-lockfile"], cwd, { YARN_ENABLE_IMMUTABLE_INSTALLS: "false" }); } @@ -110,118 +84,26 @@ function patchedPackages(cwd) { return held; } -async function outOfRange(app, lockText, direct) { - const pending = []; - for (const [name, range] of Object.entries(direct)) { - const current = resolvedDirect(app.pm, lockText, name, range); - const latest = await latestVersion(name); - if (!current || !latest || compareVersions(latest, current) <= 0) continue; - const manual = app.reactNative && MANUAL_UPGRADE.some((re) => re.test(name)); - const owner = manual - ? "Manual (Expo or MATTR SDK upgrade)" - : isMajorBump(current, latest) - ? "Dependabot major PR" - : isPinned(range, current, latest) - ? "Range is pinned, widen by hand" - : `Next refresh (released under ${MIN_AGE_DAYS} days ago)`; - pending.push({ name, range, current, latest, owner }); - } - return pending; -} - -// Exact pins and tilde ranges that exclude a newer minor. Caret ranges always allow non-major bumps. -function isPinned(range, current, latest) { - if (/^\d+\.\d+\.\d+$/.test(range)) return true; - return range.startsWith("~") && current.split(".")[1] !== latest.split(".")[1]; -} - -async function latestVersion(name) { - if (!latestCache.has(name)) { - const { ok, stdout } = run("npm", ["view", name, "dist-tags.latest", "--json"], root, {}, false); - latestCache.set(name, ok ? JSON.parse(stdout.trim() || "null") : undefined); - } - return latestCache.get(name); -} - -function auditNpm(cwd) { - const { output } = run("npm", ["audit", "--package-lock-only", "--json"], cwd); - let vulns; - try { - vulns = JSON.parse(output).vulnerabilities ?? {}; - } catch { - return [{ name: "(audit failed)", severity: "unknown", fix: tail(output, 1) }]; - } - return Object.values(vulns) - .filter((v) => v.via.some((x) => typeof x === "object")) - .map((v) => ({ name: v.name, severity: v.severity, fix: v.fixAvailable })); -} - -function describeFix(fix, current) { - if (typeof fix === "string") return fix; - if (fix === true) return "fixable in range"; - if (!fix) return "none via `npm audit fix`"; - if (current && compareVersions(fix.version, current) < 0) return `no upstream fix yet (npm would downgrade ${fix.name} to ${fix.version})`; - return `needs ${fix.name}@${fix.version}${fix.isSemVerMajor ? " (major)" : ""}`; -} - -function auditYarn(cwd) { - const { output } = run("corepack", ["yarn", "npm", "audit", "--all", "--recursive", "--json"], cwd); - const advisories = []; - for (const line of output.split("\n")) { - if (!line.startsWith("{")) continue; - try { - const { value, children } = JSON.parse(line); - advisories.push({ - name: value, - severity: children?.Severity ?? "unknown", - fix: children?.["Vulnerable Versions"] ? `vulnerable ${children["Vulnerable Versions"]}` : "", - }); - } catch {} - } - if (!advisories.length && /YN0001|Error/.test(output)) { - return [{ name: "(audit failed)", severity: "unknown", fix: tail(output, 1) }]; - } - return advisories; -} - function renderReport(results) { + const line = (c) => `- \`${c.name}\` ${c.from.join(", ") || "(new)"} → ${c.to.join(", ") || "(removed)"}`; const out = [ - "Weekly in-range refresh: every lockfile is re-resolved to the newest versions its `package.json` ranges allow. No `package.json` changes.", + "Weekly in-range refresh: every lockfile is re-resolved to the newest versions its `package.json` ranges allow, transitive dependencies included. No `package.json` changes.", "", - "| App | Direct | Transitive | Still pending after merge | Open advisories |", - "| --- | --- | --- | --- | --- |", + "| App | Direct | Transitive |", + "| --- | --- | --- |", ]; for (const r of results) { - const transitive = r.changes.length - r.directChanges.length; const flag = r.notes.length ? " (see notes)" : ""; - out.push(`| \`${r.dir}\`${flag} | ${r.directChanges.length} | ${transitive} | ${r.pending.length} | ${r.advisories.length} |`); - } - out.push("", "## Look-ahead: what still needs a human or a Dependabot PR", ""); - const pending = results.flatMap((r) => r.pending.map((p) => ({ ...p, dir: r.dir }))); - if (pending.length) { - out.push("| App | Package | Range | Locked | Latest | Who moves it |", "| --- | --- | --- | --- | --- | --- |"); - for (const p of pending) out.push(`| \`${p.dir}\` | \`${p.name}\` | \`${p.range}\` | ${p.current} | ${p.latest} | ${p.owner} |`); - } else { - out.push("Every direct dependency is on its latest release."); + out.push(`| \`${r.dir}\`${flag} | ${r.directChanges.length} | ${r.changes.length - r.directChanges.length} |`); } - out.push("", "### Advisories the refreshed lockfiles still carry", ""); - const advisories = results.flatMap((r) => r.advisories.map((a) => ({ ...a, dir: r.dir }))); - if (advisories.length) { - out.push("| App | Package | Severity | Pulled in by | Fix |", "| --- | --- | --- | --- | --- |"); - for (const a of advisories) out.push(`| \`${a.dir}\` | \`${a.name}\` | ${a.severity} | ${esc(a.via)} | ${esc(a.fix)} |`); - } else { - out.push("None."); - } - out.push("", "## Changes per app", ""); + out.push(""); for (const r of results) { if (!r.changes.length && !r.notes.length) continue; out.push(`### \`${r.dir}\``, "", ...r.notes.flatMap((n) => [`> ${n.replaceAll("\n", "\n> ")}`, ""])); - for (const c of r.directChanges) out.push(`- \`${c.name}\` ${c.from.join(", ") || "(new)"} → ${c.to.join(", ") || "(removed)"}`); + out.push(...r.directChanges.map(line)); const transitive = r.changes.filter((c) => !r.directChanges.includes(c)); if (transitive.length) { - out.push("", `
${transitive.length} transitive`, ""); - for (const c of transitive) out.push(`- \`${c.name}\` ${c.from.join(", ") || "(new)"} → ${c.to.join(", ") || "(removed)"}`); - out.push("", "
"); + out.push("", `
${transitive.length} transitive`, "", ...transitive.map(line), "", "
"); } out.push(""); } @@ -231,26 +113,19 @@ function renderReport(results) { function discoverApps() { const files = execFileSync("git", ["ls-files", "*package.json"], { cwd: root, encoding: "utf8" }).split("\n"); return files - .filter((f) => f.endsWith("package.json")) + .filter((f) => f.endsWith("/package.json")) .map((f) => f.slice(0, -"/package.json".length)) - .filter((dir) => dir && !dir.includes("node_modules")) .flatMap((dir) => { const lockfile = ["package-lock.json", "yarn.lock"].find((l) => existsSync(join(root, dir, l))); - if (!lockfile) return []; - const pm = lockfile === "yarn.lock" ? "yarn" : "npm"; - return [{ dir, lockfile, pm, reactNative: dir.startsWith("react-native-") }]; + return lockfile ? [{ dir, lockfile, pm: lockfile === "yarn.lock" ? "yarn" : "npm" }] : []; }) .sort((a, b) => a.dir.localeCompare(b.dir)); } -function run(cmd, cmdArgs, cwd, env = {}, log = true) { - if (log) console.log(`$ ${cmd} ${cmdArgs.join(" ")}`); +function run(cmd, cmdArgs, cwd, env = {}) { + console.log(`$ ${cmd} ${cmdArgs.join(" ")}`); const res = spawnSync(cmd, cmdArgs, { cwd, encoding: "utf8", maxBuffer: 256 * 1024 * 1024, env: { ...process.env, ...env } }); - return { ok: res.status === 0, output: `${res.stdout ?? ""}${res.stderr ?? ""}`, stdout: res.stdout ?? "" }; -} - -function esc(cell) { - return String(cell).replaceAll("|", "\\|"); + return { ok: res.status === 0, output: `${res.stdout ?? ""}${res.stderr ?? ""}` }; } function tail(text, lines = 15) { From bd75550104853ca1d84f3bf4b438b7b1dd52d95a Mon Sep 17 00:00:00 2001 From: Avner Matan <137777701+avner-m@users.noreply.github.com> Date: Tue, 13 Oct 2026 15:43:31 +1300 Subject: [PATCH 3/3] fix(interaction-hook-app): migrate to Tailwind CSS 4 and fix the production build --- interaction-hook-app/package-lock.json | 733 +++++++++++++++++++---- interaction-hook-app/package.json | 2 +- interaction-hook-app/postcss.config.mjs | 5 + interaction-hook-app/src/app/globals.css | 4 +- interaction-hook-app/src/app/layout.tsx | 6 +- 5 files changed, 614 insertions(+), 136 deletions(-) create mode 100644 interaction-hook-app/postcss.config.mjs diff --git a/interaction-hook-app/package-lock.json b/interaction-hook-app/package-lock.json index e5eb70c..a6460f0 100644 --- a/interaction-hook-app/package-lock.json +++ b/interaction-hook-app/package-lock.json @@ -17,10 +17,10 @@ "zod": "^3.22.4" }, "devDependencies": { + "@tailwindcss/postcss": "^4.3.3", "@types/node": "^20", "@types/react": "^18", "@types/react-dom": "^18", - "autoprefixer": "^10.0.1", "baseline-browser-mapping": "^2.10.8", "concurrently": "^9.2.1", "eslint": "^9", @@ -30,6 +30,19 @@ "typescript": "^5" } }, + "node_modules/@alloc/quick-lru": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.3.0.tgz", + "integrity": "sha512-U4+70Pc5ZS9osnCBCE5Jha/ciHM+Yp+CNMNC/7HvYbNRk1Ldd+f7qO65W5qfhu/TCv+/ozljlXXe9Nj8419DMA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@babel/code-frame": { "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", @@ -641,9 +654,6 @@ "cpu": [ "arm" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -660,9 +670,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -679,9 +686,6 @@ "cpu": [ "ppc64" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -698,9 +702,6 @@ "cpu": [ "riscv64" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -717,9 +718,6 @@ "cpu": [ "s390x" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -736,9 +734,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -755,9 +750,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -774,9 +766,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -793,9 +782,6 @@ "cpu": [ "arm" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -818,9 +804,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -843,9 +826,6 @@ "cpu": [ "ppc64" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -868,9 +848,6 @@ "cpu": [ "riscv64" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -893,9 +870,6 @@ "cpu": [ "s390x" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -918,9 +892,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -943,9 +914,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -968,9 +936,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -1226,9 +1191,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1245,9 +1207,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1264,9 +1223,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1283,9 +1239,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1620,6 +1573,277 @@ "tslib": "^2.8.0" } }, + "node_modules/@tailwindcss/node": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.3.tgz", + "integrity": "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.24.1", + "jiti": "^2.7.0", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.3.3" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.3.tgz", + "integrity": "sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.3.3", + "@tailwindcss/oxide-darwin-arm64": "4.3.3", + "@tailwindcss/oxide-darwin-x64": "4.3.3", + "@tailwindcss/oxide-freebsd-x64": "4.3.3", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.3", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.3", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.3", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.3", + "@tailwindcss/oxide-linux-x64-musl": "4.3.3", + "@tailwindcss/oxide-wasm32-wasi": "4.3.3", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.3", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.3" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.3.tgz", + "integrity": "sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.3.tgz", + "integrity": "sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.3.tgz", + "integrity": "sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.3.tgz", + "integrity": "sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.3.tgz", + "integrity": "sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.3.tgz", + "integrity": "sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.3.tgz", + "integrity": "sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.3.tgz", + "integrity": "sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.3.tgz", + "integrity": "sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.3.tgz", + "integrity": "sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.11.1", + "@emnapi/runtime": "^1.11.1", + "@emnapi/wasi-threads": "^1.2.2", + "@napi-rs/wasm-runtime": "^1.1.4", + "@tybys/wasm-util": "^0.10.2", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.3.tgz", + "integrity": "sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.3.tgz", + "integrity": "sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/postcss": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.3.3.tgz", + "integrity": "sha512-JTSZZGQi1AyKirbLN3azmjVzef92tcX7h+iSqPdaeStyFpGpDlKvvpxeOE8njhbUanbRwr3z8DyzhICWnMtQeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "@tailwindcss/node": "4.3.3", + "@tailwindcss/oxide": "4.3.3", + "postcss": "^8.5.16", + "tailwindcss": "4.3.3" + } + }, "node_modules/@tybys/wasm-util": { "version": "0.10.1", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", @@ -2501,44 +2725,6 @@ "node": ">= 0.4" } }, - "node_modules/autoprefixer": { - "version": "10.4.21", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz", - "integrity": "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/autoprefixer" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "browserslist": "^4.24.4", - "caniuse-lite": "^1.0.30001702", - "fraction.js": "^4.3.7", - "normalize-range": "^0.1.2", - "picocolors": "^1.1.1", - "postcss-value-parser": "^4.2.0" - }, - "bin": { - "autoprefixer": "bin/autoprefixer" - }, - "engines": { - "node": "^10 || ^12 || >=14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, "node_modules/available-typed-arrays": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", @@ -3033,8 +3219,8 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "devOptional": true, "license": "Apache-2.0", - "optional": true, "engines": { "node": ">=8" } @@ -3080,6 +3266,20 @@ "dev": true, "license": "MIT" }, + "node_modules/enhanced-resolve": { + "version": "5.26.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.26.0.tgz", + "integrity": "sha512-9vhedylFonb2YGogzUKX6+Ja72gOJbN1QHAqdrvqLwhdl/QWbKopzoUC9EbQNVsAns/bx/4uyqalrQAoy1IByw==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/es-abstract": { "version": "1.24.0", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", @@ -3887,20 +4087,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/fraction.js": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", - "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - }, - "funding": { - "type": "patreon", - "url": "https://github.com/sponsors/rawify" - } - }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -4088,6 +4274,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, "node_modules/has-bigints": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", @@ -4694,6 +4887,16 @@ "node": ">= 0.4" } }, + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, "node_modules/jose": { "version": "5.10.0", "resolved": "https://registry.npmjs.org/jose/-/jose-5.10.0.tgz", @@ -4839,6 +5042,267 @@ "node": ">= 0.8.0" } }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -4874,6 +5338,16 @@ "loose-envify": "cli.js" } }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -5039,16 +5513,6 @@ "dev": true, "license": "MIT" }, - "node_modules/normalize-range": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", - "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -5337,13 +5801,6 @@ "node": "^10 || ^12 || >=14" } }, - "node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true, - "license": "MIT" - }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -6092,6 +6549,20 @@ "dev": true, "license": "MIT" }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", diff --git a/interaction-hook-app/package.json b/interaction-hook-app/package.json index cc32c62..7700f6d 100644 --- a/interaction-hook-app/package.json +++ b/interaction-hook-app/package.json @@ -22,10 +22,10 @@ "zod": "^3.22.4" }, "devDependencies": { + "@tailwindcss/postcss": "^4.3.3", "@types/node": "^20", "@types/react": "^18", "@types/react-dom": "^18", - "autoprefixer": "^10.0.1", "baseline-browser-mapping": "^2.10.8", "concurrently": "^9.2.1", "eslint": "^9", diff --git a/interaction-hook-app/postcss.config.mjs b/interaction-hook-app/postcss.config.mjs new file mode 100644 index 0000000..c7bcb4b --- /dev/null +++ b/interaction-hook-app/postcss.config.mjs @@ -0,0 +1,5 @@ +const config = { + plugins: ["@tailwindcss/postcss"], +}; + +export default config; diff --git a/interaction-hook-app/src/app/globals.css b/interaction-hook-app/src/app/globals.css index 161a9bf..24c4637 100644 --- a/interaction-hook-app/src/app/globals.css +++ b/interaction-hook-app/src/app/globals.css @@ -1,6 +1,4 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; +@import "tailwindcss"; :root { --foreground-rgb: 0, 0, 0; diff --git a/interaction-hook-app/src/app/layout.tsx b/interaction-hook-app/src/app/layout.tsx index bf44f1b..6366b8e 100644 --- a/interaction-hook-app/src/app/layout.tsx +++ b/interaction-hook-app/src/app/layout.tsx @@ -1,5 +1,6 @@ import type { Metadata } from "next"; import { Inter } from "next/font/google"; +import { Suspense } from "react"; import "./globals.css"; const inter = Inter({ subsets: ["latin"] }); @@ -12,7 +13,10 @@ export const metadata: Metadata = { export default function RootLayout({ children }: { children: React.ReactNode }) { return ( - {children} + + {/* The page reads the interaction hook params with useSearchParams(). */} + {children} + ); }