Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"source": {
"source": "github",
"repo": "LasVegasForTransit/repository-tooling",
"ref": "v0.4.1"
"ref": "v0.4.2"
}
}
},
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,5 @@ blob-report/
.dev.vars
.dev.vars.*
.DS_Store
# Agent worktrees are other checkouts of this repository.
.claude/worktrees/
6 changes: 3 additions & 3 deletions .lvbt/web-platform.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
{
"formatVersion": 1,
"preset": "lvbt-web",
"release": "v0.4.1",
"commit": "0577d597f03f1036d49c497a5424ec643b75c5d1",
"contentHash": "15acec6a3332eb5a2817da55147a740549eea1b921c5dbc640658cc6226ea224",
"release": "v0.4.2",
"commit": "a841b8c34d20cb3f1ff6130213ca75d561e0ddb8",
"contentHash": "53052f1d6e00b590f189da7c7edc70210ca4ea4f09a4972ca94bb0a3351910b7",
"executables": [
"examples/with-astro/.githooks/commit-msg",
"examples/with-astro/.githooks/pre-commit",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"source": {
"source": "github",
"repo": "LasVegasForTransit/repository-tooling",
"ref": "v0.4.1"
"ref": "v0.4.2"
}
}
},
Expand Down
2 changes: 2 additions & 0 deletions .lvbt/web-platform/examples/with-astro/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,6 @@ blob-report/
.dev.vars
.dev.vars.*
.DS_Store
# Agent worktrees are other checkouts of this repository.
.claude/worktrees/
.astro/
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
// Playwright output.
"**/test-results",
"**/playwright-report",
// Agent worktrees are other checkouts of this repository.
".claude/worktrees",
"node_modules",
"**/node_modules",
Expand Down
8 changes: 4 additions & 4 deletions .lvbt/web-platform/examples/with-astro/apps/site/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,10 @@
},
"devDependencies": {
"@astrojs/check": "catalog:",
"@lasvegasfortransit/eslint-config": "0.4.1",
"@lasvegasfortransit/playwright-config": "0.4.1",
"@lasvegasfortransit/typescript-config": "0.4.1",
"@lasvegasfortransit/vitest-config": "0.4.1",
"@lasvegasfortransit/eslint-config": "0.4.2",
"@lasvegasfortransit/playwright-config": "0.4.2",
"@lasvegasfortransit/typescript-config": "0.4.2",
"@lasvegasfortransit/vitest-config": "0.4.2",
"@playwright/test": "catalog:",
"@types/node": "catalog:",
"eslint": "catalog:",
Expand Down
4 changes: 2 additions & 2 deletions .lvbt/web-platform/examples/with-astro/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@
"*": "prettier --write --ignore-unknown"
},
"devDependencies": {
"@lasvegasfortransit/cli": "0.4.1",
"@lasvegasfortransit/prettier-config": "0.4.1",
"@lasvegasfortransit/cli": "0.4.2",
"@lasvegasfortransit/prettier-config": "0.4.2",
"lint-staged": "catalog:",
"markdownlint-cli2": "catalog:",
"markdownlint-rule-relative-links": "catalog:",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"source": {
"source": "github",
"repo": "LasVegasForTransit/repository-tooling",
"ref": "v0.4.1"
"ref": "v0.4.2"
}
}
},
Expand Down
2 changes: 2 additions & 0 deletions .lvbt/web-platform/examples/with-vite-react/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,5 @@ blob-report/
.dev.vars
.dev.vars.*
.DS_Store
# Agent worktrees are other checkouts of this repository.
.claude/worktrees/
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
// Playwright output.
"**/test-results",
"**/playwright-report",
// Agent worktrees are other checkouts of this repository.
".claude/worktrees",
"node_modules",
"**/node_modules",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@
"react-dom": "catalog:"
},
"devDependencies": {
"@lasvegasfortransit/eslint-config": "0.4.1",
"@lasvegasfortransit/playwright-config": "0.4.1",
"@lasvegasfortransit/typescript-config": "0.4.1",
"@lasvegasfortransit/vitest-config": "0.4.1",
"@lasvegasfortransit/eslint-config": "0.4.2",
"@lasvegasfortransit/playwright-config": "0.4.2",
"@lasvegasfortransit/typescript-config": "0.4.2",
"@lasvegasfortransit/vitest-config": "0.4.2",
"@playwright/test": "catalog:",
"@tailwindcss/vite": "catalog:",
"@types/node": "catalog:",
Expand Down
4 changes: 2 additions & 2 deletions .lvbt/web-platform/examples/with-vite-react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@
"*": "prettier --write --ignore-unknown"
},
"devDependencies": {
"@lasvegasfortransit/cli": "0.4.1",
"@lasvegasfortransit/prettier-config": "0.4.1",
"@lasvegasfortransit/cli": "0.4.2",
"@lasvegasfortransit/prettier-config": "0.4.2",
"lint-staged": "catalog:",
"markdownlint-cli2": "catalog:",
"markdownlint-rule-relative-links": "catalog:",
Expand Down
2 changes: 1 addition & 1 deletion .lvbt/web-platform/packages/cli/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@lasvegasfortransit/cli",
"version": "0.4.1",
"version": "0.4.2",
"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",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "lvbt-contributions",
"version": "0.4.1",
"version": "0.4.2",
"description": "Create readable LVBT GitHub issues and pull requests through the organization workflow.",
"author": {
"name": "Las Vegans for Better Transit",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "lvbt-contributions",
"version": "0.4.1",
"version": "0.4.2",
"description": "Create readable LVBT GitHub issues and pull requests through the organization workflow.",
"author": {
"name": "Las Vegans for Better Transit",
Expand Down
2 changes: 1 addition & 1 deletion .lvbt/web-platform/packages/eslint-config/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@lasvegasfortransit/eslint-config",
"version": "0.4.1",
"version": "0.4.2",
"description": "The ESLint configurations every LVBT repository uses.",
"license": "MIT",
"type": "module",
Expand Down
2 changes: 1 addition & 1 deletion .lvbt/web-platform/packages/playwright-config/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@lasvegasfortransit/playwright-config",
"version": "0.4.1",
"version": "0.4.2",
"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",
Expand Down
2 changes: 1 addition & 1 deletion .lvbt/web-platform/packages/prettier-config/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@lasvegasfortransit/prettier-config",
"version": "0.4.1",
"version": "0.4.2",
"description": "The Prettier configuration every LVBT repository uses.",
"license": "MIT",
"type": "module",
Expand Down
2 changes: 1 addition & 1 deletion .lvbt/web-platform/packages/typescript-config/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@lasvegasfortransit/typescript-config",
"version": "0.4.1",
"version": "0.4.2",
"description": "TypeScript configurations every LVBT repository extends.",
"license": "MIT",
"repository": {
Expand Down
2 changes: 1 addition & 1 deletion .lvbt/web-platform/packages/vitest-config/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@lasvegasfortransit/vitest-config",
"version": "0.4.1",
"version": "0.4.2",
"description": "The Vitest configuration every LVBT repository spreads into its own.",
"license": "MIT",
"type": "module",
Expand Down
2 changes: 1 addition & 1 deletion .lvbt/web-platform/packages/web-platform/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@lasvegasfortransit/web-platform",
"version": "0.4.1",
"version": "0.4.2",
"description": "Provider-neutral deployment, provisioning, and validation primitives for LVBT web repositories.",
"license": "MIT",
"type": "module",
Expand Down
179 changes: 179 additions & 0 deletions .lvbt/web-platform/standards/consumer-ignores.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
import { readFile, writeFile } from 'node:fs/promises';
import path from 'node:path';

// Playwright writes these beside each app's configuration, such as apps/site/test-results/.
// Every example's .gitignore carries the same rules. A slash inside a pattern anchors it to the
// .gitignore's own directory, so the cache rule needs its leading **/ to reach every app.
export const PLAYWRIGHT_OUTPUT_IGNORES = [
'test-results/',
'playwright-report/',
'blob-report/',
'**/playwright/.cache/',
];

// Claude Code creates each agent worktree, a full checkout of the same repository on another
// branch, under .claude/worktrees/ inside the checkout. Git, Prettier, and markdownlint would
// otherwise treat another session's files as this repository's own.
export const AGENT_WORKTREES = '.claude/worktrees';
const AGENT_WORKTREES_REASON = 'Agent worktrees are other checkouts of this repository.';

/** The rules each root ignore file must hold, as the examples hold them. */
const LINE_RULES: Record<string, string[]> = {
'.gitignore': [...PLAYWRIGHT_OUTPUT_IGNORES, `${AGENT_WORKTREES}/`],
'.prettierignore': [`${AGENT_WORKTREES}/`],
};

const MARKDOWNLINT_CONFIG = '.markdownlint-cli2.jsonc';

/**
* Adds the standard's ignore rules that a consumer's root ignore files lack, leaving their own
* lines, comments, and order untouched. A file the consumer doesn't have stays absent.
*/
export async function syncConsumerIgnores(root: string, dryRun: boolean): Promise<string[]> {
const changed: string[] = [];
for (const [name, rules] of Object.entries(LINE_RULES)) {
if (await appendMissingLines(path.join(root, name), rules, dryRun)) changed.push(name);
}
const file = path.join(root, MARKDOWNLINT_CONFIG);
const source = await readFile(file, 'utf8').catch(() => null);
if (source !== null) {
const next = addMarkdownlintIgnore(source, AGENT_WORKTREES, AGENT_WORKTREES_REASON);
if (next !== source) {
if (!dryRun) await writeFile(file, next);
changed.push(MARKDOWNLINT_CONFIG);
}
}
return changed;
}

async function appendMissingLines(file: string, rules: string[], dryRun: boolean) {
const source = await readFile(file, 'utf8').catch(() => null);
if (source === null) return false;
const present = new Set(source.split(/\r?\n/).map((line) => line.trim()));
const missing = rules.filter((rule) => !present.has(rule));
if (missing.length === 0) return false;
const separator = source === '' || source.endsWith('\n') ? '' : '\n';
if (!dryRun) await writeFile(file, `${source}${separator}${missing.join('\n')}\n`);
return true;
}

interface Token {
text: string;
value?: string;
start: number;
end: number;
}

/** Where the comment that starts at `index` ends, or `index` when none starts there. */
function commentEnd(source: string, index: number): number {
if (source.startsWith('//', index)) {
const end = source.indexOf('\n', index);
return end === -1 ? source.length : end;
}
if (!source.startsWith('/*', index)) return index;
const end = source.indexOf('*/', index + 2);
if (end === -1) throw new Error(`${MARKDOWNLINT_CONFIG} has an unclosed comment.`);
return end + 2;
}

function stringToken(source: string, start: number): Token {
let end = start + 1;
while (end < source.length && source[end] !== '"') end += source[end] === '\\' ? 2 : 1;
if (end >= source.length) throw new Error(`${MARKDOWNLINT_CONFIG} has an unclosed string.`);
const text = source.slice(start, end + 1);
return { text, value: JSON.parse(text) as string, start, end: end + 1 };
}

/** A punctuation mark, or a literal such as `true` or `100`. */
function otherToken(source: string, start: number): Token {
const character = source.charAt(start);
if ('{}[]:,'.includes(character)) return { text: character, start, end: start + 1 };
let end = start;
while (end < source.length && !/[\s{}[\]:,"/]/.test(source.charAt(end))) end += 1;
if (end === start) throw new Error(`${MARKDOWNLINT_CONFIG} is not valid JSONC.`);
return { text: source.slice(start, end), start, end };
}

/** The strings, punctuation, and literals of a JSONC document, without whitespace or comments. */
function tokenize(source: string): Token[] {
const tokens: Token[] = [];
let index = 0;
while (index < source.length) {
const skipped = /\s/.test(source.charAt(index)) ? index + 1 : commentEnd(source, index);
if (skipped !== index) {
index = skipped;
continue;
}
const token = source[index] === '"' ? stringToken(source, index) : otherToken(source, index);
tokens.push(token);
index = token.end;
}
return tokens;
}

const NESTING = new Map([
['{', 1],
['[', 1],
['}', -1],
[']', -1],
]);

/** The `[` that opens the top-level `key` array and every token after it, if there is one. */
function topLevelArray(tokens: Token[], key: string): { open: Token; rest: Token[] } | undefined {
let depth = 0;
for (const [index, { text, value }] of tokens.entries()) {
const open = tokens[index + 2];
if (depth === 1 && value === key && tokens[index + 1]?.text === ':' && open?.text === '[')
return { open, rest: tokens.slice(index + 3) };
depth += NESTING.get(text) ?? 0;
}
return undefined;
}

/** Whether an array holds `value` as one of its own entries, given the tokens after its `[`. */
function arrayHolds(rest: Token[], value: string): boolean {
let depth = 0;
for (const token of rest) {
depth += NESTING.get(token.text) ?? 0;
if (depth < 0) return false;
if (depth === 0 && token.value === value) return true;
}
return false;
}

/** The whitespace that starts the first line after `offset` holding anything else. */
function nextIndentation(source: string, offset: number): { indentation: string; closes: boolean } {
const match = /\n([ \t]*)(\S)/.exec(source.slice(offset));
return { indentation: match?.[1] ?? '', closes: match?.[2] === ']' || match?.[2] === '}' };
}

/**
* Adds `glob` to the top-level `ignores` array of a markdownlint-cli2 configuration, with `reason`
* as a comment above it. The entry goes first in the array; every existing entry, comment, and
* blank line stays where it was. A configuration without `ignores` gets the array.
*/
export function addMarkdownlintIgnore(source: string, glob: string, reason: string): string {
const tokens = tokenize(source);
const [first] = tokens;
if (first?.text !== '{') throw new Error(`${MARKDOWNLINT_CONFIG} must hold one object.`);
const ignores = topLevelArray(tokens, 'ignores');
if (!ignores) {
const entry = `"ignores": [${JSON.stringify(glob)}],`;
return insertAfter(source, first.end, `// ${reason}`, entry);
}
if (arrayHolds(ignores.rest, glob)) return source;
return insertAfter(source, ignores.open.end, `// ${reason}`, `${JSON.stringify(glob)},`);
}

/**
* Inserts `entry` right after `offset`: on its own line below a `// comment`, indented like the
* line that follows, or ahead of the next entry when the bracket's contents share its line.
*/
function insertAfter(source: string, offset: number, comment: string, entry: string): string {
const before = source.slice(0, offset);
const after = source.slice(offset);
if (!/^[ \t]*\r?\n/.test(after)) return `${before}${entry} ${after.trimStart()}`;
const { indentation, closes } = nextIndentation(source, offset);
const indent = closes ? `${indentation} ` : indentation;
return `${before}\n${indent}${comment}\n${indent}${entry}${after}`;
}
Loading
Loading