diff --git a/.claude/settings.json b/.claude/settings.json index 470ecb4..a9cbafc 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -4,7 +4,7 @@ "source": { "source": "github", "repo": "LasVegasForTransit/repository-tooling", - "ref": "v0.7.1" + "ref": "v0.7.3" } } }, diff --git a/.lvbt/web-platform.json b/.lvbt/web-platform.json index b682377..b41e34d 100644 --- a/.lvbt/web-platform.json +++ b/.lvbt/web-platform.json @@ -1,9 +1,9 @@ { "formatVersion": 1, "preset": "lvbt-web", - "release": "v0.7.1", - "commit": "dfc9bddfc0a9c5d596c26496a9e6b6ed732d4ef3", - "contentHash": "9fc709c4ebf7edd8665cbdb81f6b5c73172b6c4fc1b8261173e975a35c359811", + "release": "v0.7.3", + "commit": "c05e7739ef28e30e0e158d9a0e6af010007ad0f0", + "contentHash": "3b3b940f842c060203bdf2f309130f4cc0a7b3d42581b98d71614f8e356c3f9a", "executables": [ "examples/with-astro/.githooks/commit-msg", "examples/with-astro/.githooks/pre-commit", diff --git a/.lvbt/web-platform/examples/with-astro/.claude/settings.json b/.lvbt/web-platform/examples/with-astro/.claude/settings.json index 470ecb4..a9cbafc 100644 --- a/.lvbt/web-platform/examples/with-astro/.claude/settings.json +++ b/.lvbt/web-platform/examples/with-astro/.claude/settings.json @@ -4,7 +4,7 @@ "source": { "source": "github", "repo": "LasVegasForTransit/repository-tooling", - "ref": "v0.7.1" + "ref": "v0.7.3" } } }, diff --git a/.lvbt/web-platform/examples/with-astro/apps/deploy/package.json b/.lvbt/web-platform/examples/with-astro/apps/deploy/package.json index 9d469d8..1325b80 100644 --- a/.lvbt/web-platform/examples/with-astro/apps/deploy/package.json +++ b/.lvbt/web-platform/examples/with-astro/apps/deploy/package.json @@ -10,9 +10,9 @@ "validate": "cf deploy --dry-run" }, "devDependencies": { - "@lasvegasfortransit/eslint-config": "0.7.1", + "@lasvegasfortransit/eslint-config": "0.7.3", "@lasvegasfortransit/site": "workspace:*", - "@lasvegasfortransit/typescript-config": "0.7.1", + "@lasvegasfortransit/typescript-config": "0.7.3", "cf": "catalog:", "eslint": "catalog:", "typescript": "catalog:", diff --git a/.lvbt/web-platform/examples/with-astro/apps/site/package.json b/.lvbt/web-platform/examples/with-astro/apps/site/package.json index 956b857..e1f6201 100644 --- a/.lvbt/web-platform/examples/with-astro/apps/site/package.json +++ b/.lvbt/web-platform/examples/with-astro/apps/site/package.json @@ -23,10 +23,10 @@ }, "devDependencies": { "@astrojs/check": "catalog:", - "@lasvegasfortransit/eslint-config": "0.7.1", - "@lasvegasfortransit/playwright-config": "0.7.1", - "@lasvegasfortransit/typescript-config": "0.7.1", - "@lasvegasfortransit/vitest-config": "0.7.1", + "@lasvegasfortransit/eslint-config": "0.7.3", + "@lasvegasfortransit/playwright-config": "0.7.3", + "@lasvegasfortransit/typescript-config": "0.7.3", + "@lasvegasfortransit/vitest-config": "0.7.3", "@playwright/test": "catalog:", "@types/node": "catalog:", "eslint": "catalog:", diff --git a/.lvbt/web-platform/examples/with-astro/package.json b/.lvbt/web-platform/examples/with-astro/package.json index cc8fcf7..c2c4cd4 100644 --- a/.lvbt/web-platform/examples/with-astro/package.json +++ b/.lvbt/web-platform/examples/with-astro/package.json @@ -28,8 +28,8 @@ "*": "prettier --write --ignore-unknown" }, "devDependencies": { - "@lasvegasfortransit/cli": "0.7.1", - "@lasvegasfortransit/prettier-config": "0.7.1", + "@lasvegasfortransit/cli": "0.7.3", + "@lasvegasfortransit/prettier-config": "0.7.3", "lint-staged": "catalog:", "markdownlint-cli2": "catalog:", "markdownlint-rule-relative-links": "catalog:", @@ -39,7 +39,7 @@ "typescript": "catalog:", "wrangler": "catalog:", "tsx": "catalog:", - "@lasvegasfortransit/web-platform": "0.7.1" + "@lasvegasfortransit/web-platform": "0.7.3" }, "packageManager": "pnpm@11.25.0", "engines": { diff --git a/.lvbt/web-platform/examples/with-vite-react/.claude/settings.json b/.lvbt/web-platform/examples/with-vite-react/.claude/settings.json index 470ecb4..a9cbafc 100644 --- a/.lvbt/web-platform/examples/with-vite-react/.claude/settings.json +++ b/.lvbt/web-platform/examples/with-vite-react/.claude/settings.json @@ -4,7 +4,7 @@ "source": { "source": "github", "repo": "LasVegasForTransit/repository-tooling", - "ref": "v0.7.1" + "ref": "v0.7.3" } } }, diff --git a/.lvbt/web-platform/examples/with-vite-react/apps/app/package.json b/.lvbt/web-platform/examples/with-vite-react/apps/app/package.json index 11e8fe6..a34bfb1 100644 --- a/.lvbt/web-platform/examples/with-vite-react/apps/app/package.json +++ b/.lvbt/web-platform/examples/with-vite-react/apps/app/package.json @@ -19,10 +19,10 @@ "react-dom": "catalog:" }, "devDependencies": { - "@lasvegasfortransit/eslint-config": "0.7.1", - "@lasvegasfortransit/playwright-config": "0.7.1", - "@lasvegasfortransit/typescript-config": "0.7.1", - "@lasvegasfortransit/vitest-config": "0.7.1", + "@lasvegasfortransit/eslint-config": "0.7.3", + "@lasvegasfortransit/playwright-config": "0.7.3", + "@lasvegasfortransit/typescript-config": "0.7.3", + "@lasvegasfortransit/vitest-config": "0.7.3", "@playwright/test": "catalog:", "@tailwindcss/vite": "catalog:", "@types/node": "catalog:", diff --git a/.lvbt/web-platform/examples/with-vite-react/apps/deploy/package.json b/.lvbt/web-platform/examples/with-vite-react/apps/deploy/package.json index 3fa5396..0bc8fe7 100644 --- a/.lvbt/web-platform/examples/with-vite-react/apps/deploy/package.json +++ b/.lvbt/web-platform/examples/with-vite-react/apps/deploy/package.json @@ -10,9 +10,9 @@ "validate": "cf deploy --dry-run" }, "devDependencies": { - "@lasvegasfortransit/eslint-config": "0.7.1", + "@lasvegasfortransit/eslint-config": "0.7.3", "@lasvegasfortransit/app": "workspace:*", - "@lasvegasfortransit/typescript-config": "0.7.1", + "@lasvegasfortransit/typescript-config": "0.7.3", "cf": "catalog:", "eslint": "catalog:", "typescript": "catalog:", diff --git a/.lvbt/web-platform/examples/with-vite-react/package.json b/.lvbt/web-platform/examples/with-vite-react/package.json index 3b251f9..449dc65 100644 --- a/.lvbt/web-platform/examples/with-vite-react/package.json +++ b/.lvbt/web-platform/examples/with-vite-react/package.json @@ -28,8 +28,8 @@ "*": "prettier --write --ignore-unknown" }, "devDependencies": { - "@lasvegasfortransit/cli": "0.7.1", - "@lasvegasfortransit/prettier-config": "0.7.1", + "@lasvegasfortransit/cli": "0.7.3", + "@lasvegasfortransit/prettier-config": "0.7.3", "lint-staged": "catalog:", "markdownlint-cli2": "catalog:", "markdownlint-rule-relative-links": "catalog:", @@ -38,7 +38,7 @@ "typescript": "catalog:", "wrangler": "catalog:", "tsx": "catalog:", - "@lasvegasfortransit/web-platform": "0.7.1" + "@lasvegasfortransit/web-platform": "0.7.3" }, "packageManager": "pnpm@11.25.0", "engines": { diff --git a/.lvbt/web-platform/packages/cli/package.json b/.lvbt/web-platform/packages/cli/package.json index c150bff..f98fb83 100644 --- a/.lvbt/web-platform/packages/cli/package.json +++ b/.lvbt/web-platform/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@lasvegasfortransit/cli", - "version": "0.7.1", + "version": "0.7.3", "description": "The lvbt command every LVBT repository runs for bootstrap, preflight, and deploy, plus the production platform setup, the shared git hooks, and the lvbt-contributions agent plugin.", "license": "MIT", "type": "module", diff --git a/.lvbt/web-platform/packages/cli/plugins/lvbt-contributions/.claude-plugin/plugin.json b/.lvbt/web-platform/packages/cli/plugins/lvbt-contributions/.claude-plugin/plugin.json index 38058b2..15f79cc 100644 --- a/.lvbt/web-platform/packages/cli/plugins/lvbt-contributions/.claude-plugin/plugin.json +++ b/.lvbt/web-platform/packages/cli/plugins/lvbt-contributions/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "lvbt-contributions", - "version": "0.7.1", + "version": "0.7.3", "description": "Create readable LVBT contributions and reconcile trusted audit and recurring issues through the organization workflow.", "author": { "name": "Las Vegans for Better Transit", diff --git a/.lvbt/web-platform/packages/cli/plugins/lvbt-contributions/.codex-plugin/plugin.json b/.lvbt/web-platform/packages/cli/plugins/lvbt-contributions/.codex-plugin/plugin.json index 9e69a2a..f458240 100644 --- a/.lvbt/web-platform/packages/cli/plugins/lvbt-contributions/.codex-plugin/plugin.json +++ b/.lvbt/web-platform/packages/cli/plugins/lvbt-contributions/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "lvbt-contributions", - "version": "0.7.1", + "version": "0.7.3", "description": "Create readable LVBT contributions and reconcile trusted audit and recurring issues through the organization workflow.", "author": { "name": "Las Vegans for Better Transit", diff --git a/.lvbt/web-platform/packages/cli/src/lib/release/runner.mjs b/.lvbt/web-platform/packages/cli/src/lib/release/runner.mjs index 81b0391..db8b7c2 100644 --- a/.lvbt/web-platform/packages/cli/src/lib/release/runner.mjs +++ b/.lvbt/web-platform/packages/cli/src/lib/release/runner.mjs @@ -2,7 +2,7 @@ import { spawn } from 'node:child_process'; import { access } from 'node:fs/promises'; import path from 'node:path'; import { createRequire } from 'node:module'; -import { fileURLToPath } from 'node:url'; +import { fileURLToPath, pathToFileURL } from 'node:url'; import { CliError } from '../arguments.mjs'; import { readTooling } from '../tooling.mjs'; @@ -37,10 +37,15 @@ export async function runRelease({ cwd, mode, args = [] }) { throw new CliError('Configure release in .lvbt/tooling.json before releasing.', 2); const entry = await releaseEntry(cwd); await new Promise((resolve, reject) => { - const child = spawn('pnpm', ['exec', 'tsx', entry, mode, cwd, ...args], { - cwd, - stdio: 'inherit', - }); + const runtime = createRequire(import.meta.url).resolve('tsx'); + const child = spawn( + process.execPath, + ['--import', pathToFileURL(runtime).href, entry, mode, cwd, ...args], + { + cwd, + stdio: 'inherit', + }, + ); child.once('error', reject); child.once('exit', (code, signal) => code === 0 diff --git a/.lvbt/web-platform/packages/eslint-config/package.json b/.lvbt/web-platform/packages/eslint-config/package.json index 96a7546..dff3034 100644 --- a/.lvbt/web-platform/packages/eslint-config/package.json +++ b/.lvbt/web-platform/packages/eslint-config/package.json @@ -1,6 +1,6 @@ { "name": "@lasvegasfortransit/eslint-config", - "version": "0.7.1", + "version": "0.7.3", "description": "The ESLint configurations every LVBT repository uses.", "license": "MIT", "type": "module", diff --git a/.lvbt/web-platform/packages/playwright-config/package.json b/.lvbt/web-platform/packages/playwright-config/package.json index 7161be1..de076a8 100644 --- a/.lvbt/web-platform/packages/playwright-config/package.json +++ b/.lvbt/web-platform/packages/playwright-config/package.json @@ -1,6 +1,6 @@ { "name": "@lasvegasfortransit/playwright-config", - "version": "0.7.1", + "version": "0.7.3", "description": "The Playwright configuration every LVBT repository spreads into its own: end-to-end tests under tests/e2e, desktop and mobile projects, traces on failure.", "license": "MIT", "type": "module", diff --git a/.lvbt/web-platform/packages/prettier-config/package.json b/.lvbt/web-platform/packages/prettier-config/package.json index df07b57..973da7d 100644 --- a/.lvbt/web-platform/packages/prettier-config/package.json +++ b/.lvbt/web-platform/packages/prettier-config/package.json @@ -1,6 +1,6 @@ { "name": "@lasvegasfortransit/prettier-config", - "version": "0.7.1", + "version": "0.7.3", "description": "The Prettier configuration every LVBT repository uses.", "license": "MIT", "type": "module", diff --git a/.lvbt/web-platform/packages/typescript-config/package.json b/.lvbt/web-platform/packages/typescript-config/package.json index 1c88eee..39c5ecd 100644 --- a/.lvbt/web-platform/packages/typescript-config/package.json +++ b/.lvbt/web-platform/packages/typescript-config/package.json @@ -1,6 +1,6 @@ { "name": "@lasvegasfortransit/typescript-config", - "version": "0.7.1", + "version": "0.7.3", "description": "TypeScript configurations every LVBT repository extends.", "license": "MIT", "repository": { diff --git a/.lvbt/web-platform/packages/vitest-config/package.json b/.lvbt/web-platform/packages/vitest-config/package.json index 3570548..eb4a888 100644 --- a/.lvbt/web-platform/packages/vitest-config/package.json +++ b/.lvbt/web-platform/packages/vitest-config/package.json @@ -1,6 +1,6 @@ { "name": "@lasvegasfortransit/vitest-config", - "version": "0.7.1", + "version": "0.7.3", "description": "The Vitest configuration every LVBT repository spreads into its own.", "license": "MIT", "type": "module", diff --git a/.lvbt/web-platform/packages/web-platform/package.json b/.lvbt/web-platform/packages/web-platform/package.json index 2e4906c..7dcc837 100644 --- a/.lvbt/web-platform/packages/web-platform/package.json +++ b/.lvbt/web-platform/packages/web-platform/package.json @@ -1,6 +1,6 @@ { "name": "@lasvegasfortransit/web-platform", - "version": "0.7.1", + "version": "0.7.3", "description": "Provider-neutral deployment, provisioning, and validation primitives for LVBT web repositories.", "license": "MIT", "type": "module", diff --git a/.lvbt/web-platform/standards/process-contract.ts b/.lvbt/web-platform/standards/process-contract.ts index be4bd93..a77d34f 100644 --- a/.lvbt/web-platform/standards/process-contract.ts +++ b/.lvbt/web-platform/standards/process-contract.ts @@ -47,7 +47,8 @@ export function inventoryPaths(paths: string[]): string[] { ...paths.filter( (file) => !file.startsWith('.lvbt/web-platform/') && - (configurationFamily(file) !== undefined || + (file.endsWith('/package.json') || + configurationFamily(file) !== undefined || /^\.github\/workflows\/[^/]+\.ya?ml$/u.test(file)), ), ]), @@ -60,6 +61,33 @@ function json(input: string | null | undefined): unknown { return undefined; } } +/** Resolve only declared local modules; an unproven external wrapper remains a diagnostic. */ +export function configurationTargets(files: ProcessSnapshot['files'], file: string): string[] { + const targets: string[] = []; + const content = files[file] ?? ''; + for (const match of content.matchAll(/['"]([^'"]+)['"]/gu)) { + const specifier = match[1] ?? ''; + if (specifier.startsWith('.')) { + const base = path.posix.normalize(path.posix.join(path.posix.dirname(file), specifier)); + targets.push(base, `${base}.json`, `${base}.js`, `${base}.mjs`, `${base}.ts`); + if (base.endsWith('.js')) targets.push(base.slice(0, -3) + '.ts'); + continue; + } + for (const [manifest, source] of Object.entries(files)) { + if (!manifest.endsWith('/package.json')) continue; + const pkg = json(source) as { name?: string; exports?: Record } | undefined; + if (!pkg?.name || !specifier.startsWith(`${pkg.name}/`)) continue; + const entry = pkg.exports?.[`./${specifier.slice(pkg.name.length + 1)}`]; + const visit = (value: unknown): void => { + if (typeof value === 'string' && value.startsWith('./')) + targets.push(path.posix.normalize(path.posix.join(path.posix.dirname(manifest), value))); + else if (value && typeof value === 'object') Object.values(value).forEach(visit); + }; + visit(entry); + } + } + return [...new Set(targets)]; +} function sharedConfiguration( files: ProcessSnapshot['files'], file: string, @@ -71,16 +99,9 @@ function sharedConfiguration( const content = files[file]; if (!content) return false; if (content.includes(`@lasvegasfortransit/${family}-config`)) return true; - for (const match of content.matchAll(/['"](\.[^'"]+)['"]/gu)) { - const base = path.posix.normalize(path.posix.join(path.posix.dirname(file), match[1] ?? '')); - if ( - [base, `${base}.json`, `${base}.js`, `${base}.mjs`, `${base}.ts`].some((target) => - sharedConfiguration(files, target, family, seen), - ) - ) - return true; - } - return false; + return configurationTargets(files, file).some((target) => + sharedConfiguration(files, target, family, seen), + ); } function commandFindings( snapshot: ProcessSnapshot, @@ -141,7 +162,12 @@ function auditFindings( !tooling?.audits || !audit.includes('schedule:') || !audit.includes('workflow_dispatch:') || - !/audit\s+(?:report|--target\s+production)/u.test(audit) + !( + /audit\s+(?:report|--target\s+production)/u.test(audit) || + /^[\t ]*uses:[\t ]*([\x22\x27]?)LasVegasForTransit\/repository-tooling\/\.github\/workflows\/audit\.ya?ml@[a-f0-9]{40}\1[\t ]*(?:#.*)?$/mu.test( + audit, + ) + ) ) add( 'audit-workflow', diff --git a/.lvbt/web-platform/standards/propagate.ts b/.lvbt/web-platform/standards/propagate.ts index 62a5764..8039af8 100644 --- a/.lvbt/web-platform/standards/propagate.ts +++ b/.lvbt/web-platform/standards/propagate.ts @@ -169,7 +169,7 @@ export const run: Runner = (command, args, cwd) => cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'inherit'], - }).trim(); + }).trimEnd(); async function currentRelease(target: string): Promise { const file = path.join(target, '.lvbt/web-platform.json'); @@ -206,6 +206,7 @@ async function publishTemplate(options: { entry.example, '--release', tag, + '--skip-install', ], target, ); @@ -260,7 +261,7 @@ export async function applyRelease(options: { ); else throw new Error(`${entry.name} does not receive releases.`); - if (install) runner('pnpm', ['install', '--lockfile-only', '--no-frozen-lockfile'], target); + if (install) runner('pnpm', ['install', '--no-frozen-lockfile'], target); const skippedWorkflows = skipWorkflows ? restoreWorkflows(target, runner) : []; if (!runner('git', ['status', '--porcelain'], target)) return { changed: false, from: release, skippedWorkflows }; diff --git a/.lvbt/web-platform/standards/status.ts b/.lvbt/web-platform/standards/status.ts index 1c1fd6e..ca3ef8c 100644 --- a/.lvbt/web-platform/standards/status.ts +++ b/.lvbt/web-platform/standards/status.ts @@ -13,7 +13,13 @@ import { latestRelease, readRegistry, } from './propagate.ts'; -import { inventoryPaths, processFindings, type ProcessSnapshot } from './process-contract.ts'; +import { + configurationTargets, + configurationFamily, + inventoryPaths, + processFindings, + type ProcessSnapshot, +} from './process-contract.ts'; import { OWNED_FILES } from './owned-files.ts'; /** Days a repository may trail the latest release while its update pull request runs. */ @@ -168,7 +174,7 @@ export function pluginRef(settings: string | null): string | null { function readFiles(repository: string, commit: string, paths: string[], read: typeof gh) { const requested = inventoryPaths(paths); requested.push(...OWNED_FILES.map((file) => `.lvbt/web-platform/examples/with-astro/${file}`)); - return Object.fromEntries( + const files = Object.fromEntries( requested.map((file) => { const content = paths.includes(file) ? readRaw(repository, file, commit, read) : null; if (paths.includes(file) && content === null) @@ -178,6 +184,22 @@ function readFiles(repository: string, commit: string, paths: string[], read: ty return [file, content]; }), ); + let pending = Object.keys(files).filter((file) => configurationFamily(file) !== undefined); + while (pending.length) { + const next = [...new Set(pending.flatMap((file) => configurationTargets(files, file)))].filter( + (file) => paths.includes(file) && !Object.hasOwn(files, file), + ); + for (const file of next) { + const content = readRaw(repository, file, commit, read); + if (content === null) + throw new Error( + `Cannot inventory ${repository}/${file} at ${commit}; GitHub contents are unavailable.`, + ); + files[file] = content; + } + pending = next; + } + return files; } export function readState(entry: RegistryEntry, read = gh): RepositoryState { const repository = JSON.parse(read(['api', `repos/${OWNER}/${entry.name}`])) as { diff --git a/.lvbt/web-platform/standards/template-publication.ts b/.lvbt/web-platform/standards/template-publication.ts index 874839a..3c52934 100644 --- a/.lvbt/web-platform/standards/template-publication.ts +++ b/.lvbt/web-platform/standards/template-publication.ts @@ -1,5 +1,5 @@ import { execFileSync } from 'node:child_process'; -import { realpathSync } from 'node:fs'; +import { existsSync, realpathSync } from 'node:fs'; import { cp, readFile, readdir, rm, writeFile } from 'node:fs/promises'; import path from 'node:path'; import { pathToFileURL } from 'node:url'; @@ -111,6 +111,7 @@ export async function main(args: string[]): Promise { target: { type: 'string' }, example: { type: 'string' }, release: { type: 'string' }, + 'skip-install': { type: 'boolean', default: false }, }, }); if (!values.source || !values.target || !values.example || !values.release) { @@ -118,12 +119,24 @@ export async function main(args: string[]): Promise { 'Usage: --source --target --example --release ', ); } + // Published update drivers run this incoming entry point before their lockfile-only install. + // Restore real hooks for those drivers; current callers explicitly own installation themselves. + const lockfile = path.join(values.target, 'pnpm-lock.yaml'); + const published = + !values['skip-install'] && existsSync(lockfile) ? await readFile(lockfile, 'utf8') : undefined; await materializeTemplate({ source: values.source, target: values.target, example: values.example, release: values.release, }); + if (!values['skip-install']) { + if (published !== undefined) await writeFile(lockfile, published); + execFileSync('pnpm', ['install', '--no-frozen-lockfile'], { + cwd: values.target, + stdio: 'inherit', + }); + } } if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) {