Skip to content
Open
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 .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -1 +1 @@
npx lint-staged && npm run pre-commit:encrypt
npx lint-staged && npm run pre-commit:encrypt && bash cookbook/scripts/pre-commit.sh
125 changes: 119 additions & 6 deletions cookbook/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@
- [Schema](#schema)
- [Syntax](#syntax-6)
- [Example](#example-6)
- [Skeleton Files](#skeleton-files)
- [Syntax](#syntax-7)
- [Example](#example-7)
- [Affected Recipes](#affected-recipes)
- [Syntax](#syntax-8)
- [Example](#example-8)

This is the Hydrogen Cookbook, a collection of example _recipes_ to showcase specific scenarios and usecases for Hydrogen projects.

Expand All @@ -52,12 +58,14 @@ The cookbook comes with a set of commands for creating and managing recipes. All
cookbook.ts <command>

Commands:
cookbook.ts generate Generate a recipe from the skeleton's changes
cookbook.ts render Render a recipe to a given format
cookbook.ts apply Apply a recipe to the current project
cookbook.ts validate Validate a recipe
cookbook.ts regenerate Regenerate a recipe
cookbook.ts update Update a recipe
cookbook.ts generate Generate a recipe from the skeleton's changes
cookbook.ts render Render a recipe to a given format
cookbook.ts apply Apply a recipe to the current project
cookbook.ts validate Validate a recipe
cookbook.ts regenerate Regenerate a recipe
cookbook.ts update Update a recipe
cookbook.ts skeleton-files List all skeleton files referenced by recipes
cookbook.ts affected-recipes List recipes affected by changes to skeleton files

Options:
--version Show version number [boolean]
Expand Down Expand Up @@ -283,3 +291,108 @@ Options:
```sh
npm run cookbook -- schema
```

### Skeleton Files

`skeleton-files` lists all skeleton template files that are referenced by recipes (via diffs or ingredients). It maps each file to the recipes that touch it.

Note: `deletedFiles` entries in a recipe are intentionally excluded — files that are unconditionally deleted by a recipe cannot become stale and do not need to be tracked.

#### Syntax

```plain
cookbook.ts skeleton-files

List all skeleton files referenced by recipes (or a specific recipe)

Options:
--version Show version number [boolean]
--help Show help [boolean]
--recipe Recipe name(s) to query (default: all recipes) [array]
--json Output as JSON object instead of formatted text
[boolean] [default: false]
--existing-only Only show files that currently exist in the skeleton
[boolean] [default: false]
```

#### Example

```sh
# List all skeleton files referenced by any recipe
npm run cookbook -- skeleton-files

# List files for a specific recipe
npm run cookbook -- skeleton-files --recipe my-recipe

# List files for multiple recipes, as JSON
npm run cookbook -- skeleton-files --recipe recipe-a --recipe recipe-b --json

# Only show files that currently exist on disk
npm run cookbook -- skeleton-files --existing-only
```

Default (text) output format:

```
templates/skeleton/app/root.tsx -> [recipe-a, recipe-b]
templates/skeleton/app/server.ts -> [recipe-a]
```

JSON output format (`--json`):

```json
{
"templates/skeleton/app/root.tsx": ["recipe-a", "recipe-b"],
"templates/skeleton/app/server.ts": ["recipe-a"]
}
```

### Affected Recipes

`affected-recipes` takes a list of changed skeleton file paths and returns the names of all recipes that reference any of those files. It is called by the pre-commit hook to warn developers when skeleton changes may require recipe updates.

Note: `deletedFiles` entries in a recipe are intentionally excluded — changes to files that a recipe unconditionally deletes cannot make the recipe stale.

#### Syntax

```plain
cookbook.ts affected-recipes [files..]

List recipes affected by changes to the given skeleton files

Options:
--version Show version number [boolean]
--help Show help [boolean]
--files Repo-relative paths to changed skeleton files
[array] [default: []]
--json Output as JSON array instead of newline-separated names
[boolean] [default: false]
```

#### Example

```sh
# Check which recipes are affected by a changed file
npm run cookbook -- affected-recipes templates/skeleton/app/root.tsx

# Pass multiple files
npm run cookbook -- affected-recipes \
templates/skeleton/app/root.tsx \
templates/skeleton/app/server.ts

# Output as a JSON array (useful for scripting / CI matrix jobs)
npm run cookbook -- affected-recipes --json templates/skeleton/app/root.tsx
```

Default (text) output — one recipe name per line:

```
recipe-a
recipe-b
```

JSON output (`--json`):

```json
["recipe-a", "recipe-b"]
```
53 changes: 53 additions & 0 deletions cookbook/scripts/pre-commit.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
#!/bin/bash
# Detects skeleton file changes and warns about recipes that may need updating.
# Always exits 0 — this check is informational only.
# Bypass: git commit --no-verify

REPO_ROOT="$(git rev-parse --show-toplevel)"

# 1. Find staged skeleton files (read into array to safely handle spaces/special chars)
STAGED_ARGS=()
while IFS= read -r f; do
[[ -n "$f" ]] && STAGED_ARGS+=("$f")
done < <(git diff --cached --name-only -- 'templates/skeleton/')

if [ ${#STAGED_ARGS[@]} -eq 0 ]; then
exit 0
fi

# 2. Find affected recipes (one name per line)
# Capture stderr separately so errors surface as warnings without mixing into output.
STDERR_FILE=$(mktemp)
AFFECTED=$(cd "$REPO_ROOT/cookbook" && npm run --silent cookbook -- affected-recipes "${STAGED_ARGS[@]}" 2>"$STDERR_FILE")
STATUS=$?
STDERR_CONTENT=$(cat "$STDERR_FILE")
rm -f "$STDERR_FILE"

if [ $STATUS -ne 0 ]; then
echo "⚠️ Skeleton recipe check encountered an error — skipping." >&2
[ -n "$STDERR_CONTENT" ] && echo "$STDERR_CONTENT" >&2
exit 0
fi

if [ -z "$AFFECTED" ]; then
exit 0
fi

# 3. Print actionable warning
echo ""
echo "⚠️ Skeleton changes detected. The following recipes may need updating:"
echo ""
while IFS= read -r recipe; do
echo " - $recipe"
done <<< "$AFFECTED"
echo ""
echo " After committing your skeleton changes, run the following to update affected recipes:"
while IFS= read -r recipe; do
echo " cd cookbook && npm run cookbook -- regenerate --recipe $recipe --format github"
done <<< "$AFFECTED"
echo ""
echo " Note: the regenerate command requires a clean working tree — run it after committing."
echo " To skip this skeleton changes warning: git commit --no-verify"
echo ""

exit 0
72 changes: 72 additions & 0 deletions cookbook/src/commands/affected-recipes.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import {describe, expect, it, vi, beforeEach} from 'vitest';
import {affectedRecipes} from './affected-recipes';

vi.mock('../lib/dependency-graph', () => ({getAffectedRecipes: vi.fn()}));

import {getAffectedRecipes} from '../lib/dependency-graph';

const mockGetAffectedRecipes = vi.mocked(getAffectedRecipes);

// Extract the handler for direct invocation in tests
const handler = affectedRecipes.handler as (args: {
files: string[];
json: boolean;
}) => void;

describe('affected-recipes command handler', () => {
beforeEach(() => {
vi.clearAllMocks();
vi.spyOn(console, 'log').mockImplementation(() => {});
});

it('passes the files array to getAffectedRecipes', () => {
mockGetAffectedRecipes.mockReturnValue([]);
const files = [
'templates/skeleton/app/root.tsx',
'templates/skeleton/app/server.ts',
];

handler({files, json: false});

expect(mockGetAffectedRecipes).toHaveBeenCalledWith(files);
});

it('prints one recipe name per line by default', () => {
mockGetAffectedRecipes.mockReturnValue(['multipass', 'b2b', 'markets']);

handler({files: ['templates/skeleton/app/root.tsx'], json: false});

expect(console.log).toHaveBeenCalledTimes(3);
expect(console.log).toHaveBeenNthCalledWith(1, 'multipass');
expect(console.log).toHaveBeenNthCalledWith(2, 'b2b');
expect(console.log).toHaveBeenNthCalledWith(3, 'markets');
});

it('prints JSON array when --json flag is set', () => {
mockGetAffectedRecipes.mockReturnValue(['multipass', 'b2b']);

handler({files: ['templates/skeleton/app/root.tsx'], json: true});

expect(console.log).toHaveBeenCalledTimes(1);
expect(console.log).toHaveBeenCalledWith(
JSON.stringify(['multipass', 'b2b']),
);
});

it('produces no output when no recipes are affected', () => {
mockGetAffectedRecipes.mockReturnValue([]);

handler({files: ['templates/skeleton/app/root.tsx'], json: false});

expect(console.log).not.toHaveBeenCalled();
});

it('prints empty JSON array when --json flag is set and no recipes are affected', () => {
mockGetAffectedRecipes.mockReturnValue([]);

handler({files: ['templates/skeleton/app/root.tsx'], json: true});

expect(console.log).toHaveBeenCalledTimes(1);
expect(console.log).toHaveBeenCalledWith(JSON.stringify([]));
});
});
36 changes: 36 additions & 0 deletions cookbook/src/commands/affected-recipes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import {CommandModule} from 'yargs';
import {getAffectedRecipes} from '../lib/dependency-graph';

type AffectedRecipesArgs = {
files: string[];
json: boolean;
};

export const affectedRecipes: CommandModule<{}, AffectedRecipesArgs> = {
command: 'affected-recipes [files..]',
describe: 'List recipes affected by changes to the given skeleton files',
builder: {
files: {
type: 'array',
string: true,
description: 'Repo-relative paths to changed skeleton files',
default: [],
},
json: {
type: 'boolean',
description: 'Output as JSON array instead of newline-separated names',
default: false,
},
},
handler({files, json}) {
const affected = getAffectedRecipes(files);

if (json) {
console.log(JSON.stringify(affected));
} else {
for (const name of affected) {
console.log(name);
}
}
},
};
2 changes: 2 additions & 0 deletions cookbook/src/commands/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
export {affectedRecipes} from './affected-recipes';
export {apply} from './apply';
export {generate} from './generate';
export {regenerate} from './regenerate';
export {render} from './render';
export {skeletonFiles} from './skeleton-files';
export {update} from './update';
export {validate} from './validate';
export {schema} from './schema';
Loading
Loading