Skip to content

Fix Recipes failing due to pnpm workspace/catalog in skeleton and line number drifts#3552

Open
andguy95 wants to merge 6 commits intomainfrom
an-fix-recipes
Open

Fix Recipes failing due to pnpm workspace/catalog in skeleton and line number drifts#3552
andguy95 wants to merge 6 commits intomainfrom
an-fix-recipes

Conversation

@andguy95
Copy link
Collaborator

@andguy95 andguy95 commented Mar 6, 2026

WHY are these changes introduced?

The Hydrogen monorepo recently adopted pnpm's catalog: and workspace: protocols in pnpm-workspace.yaml for managing dependency versions across packages. The cookbook recipe validation system uses npm install to verify that recipes apply cleanly to the skeleton template, but npm doesn't understand these pnpm-specific protocols, causing all recipe validations to fail. Additionally, many recipe patches had drifted due to recent skeleton template changes (line number shifts, content changes. Potentially from prettier fix.).

WHAT is this pull request doing?

1. Adds catalog: and workspace: protocol resolution to the cookbook validation system

  • Introduces resolveCatalogProtocol() in cookbook/src/lib/validate.ts that reads pnpm-workspace.yaml and replaces catalog: / catalog:default references with actual versions from the catalog, and workspace:* references with the package's real version from the monorepo
  • Resolves these protocols across all workspace package.json files before running npm install, since npm traverses the workspace tree
  • Restores modified package.json files in the finally block after validation completes
  • Updates npm install to use --no-workspaces --ignore-scripts flags to avoid workspace-related issues during validation
  • Fixes the dirty-check in apply.ts to use path.basename(f) so package.json files in subdirectories are also properly ignored

2. Calls resolveCatalogProtocol() in the apply and regenerate commands so patches can be regenerated correctly against the resolved skeleton.

3. Fixes stale patches across multiple recipes:

  • b2b, bundles, combined-listings, express, legacy-customer-account-flow, markets, metaobjects, multipass, partytown, subscriptions, third-party-queries-caching
  • Updates patch context lines and offsets to match current skeleton template content
  • Adds a new tsconfig.json patch for the express recipe

4. Adds a Claude Code command (.claude/commands/fix-cookbook-recipe.md) for automating recipe patch fixes when skeleton template changes cause validation drift.

5. Adds tests for the new resolveCatalogProtocol functionality covering catalog resolution, workspace resolution, catalog:default syntax, and edge cases.

HOW to test your changes?

  1. From the cookbook/ directory, run validation for all recipes:
    cd cookbook && npx tsx src/index.ts validate --recipe all
  2. Run the new unit tests:
    cd cookbook && npx vitest run src/lib/validate.test.ts
  3. Verify individual recipe validation works:
    cd cookbook && npx tsx src/index.ts validate --recipe bundles

Post-merge steps

None required.

Checklist

  • I've read the Contributing Guidelines
  • I've considered possible cross-platform impacts (Mac, Linux, Windows)
  • I've added a changeset if this PR contains user-facing or noteworthy changes
  • I've added tests to cover my changes
  • I've added or updated the documentation

@andguy95 andguy95 requested a review from a team as a code owner March 6, 2026 21:44
@shopify
Copy link
Contributor

shopify bot commented Mar 6, 2026

Oxygen deployed a preview of your an-fix-recipes branch. Details:

Storefront Status Preview link Deployment details Last update (UTC)
Skeleton (skeleton.hydrogen.shop) ✅ Successful (Logs) Preview deployment Inspect deployment March 10, 2026 6:49 PM

Learn more about Hydrogen's GitHub integration.

execSync(`git checkout -- ${modifiedPkgJsons.split('\n').join(' ')}`, {
cwd: REPO_ROOT,
stdio: 'pipe',
});

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cleanup uses unsafe shell string construction to restore modified package.json files

In finally, restoration runs execSync(git checkout -- ${modifiedPkgJsons.split('\n').join(' ')}), concatenating unescaped filenames into a shell command.

Impact: breaks with spaces/shell-special characters in paths and can leave the repo dirty. It’s also generally unsafe (potential injection in hostile environments). Cleanup failures are particularly harmful because they defeat the guarantee of restoring modified files.

@binks-code-reviewer
Copy link

binks-code-reviewer bot commented Mar 6, 2026

🤖 Code Review · #projects-dev-ai for questions
React with 👍/👎 or reply — all feedback helps improve the agent.

Complete - No issues

📋 History

✅ 1 findings → ✅ No issues

};

function handler(args: ApplyArgs) {
resolveCatalogProtocol();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

blocking: resolveCatalogProtocol() modifies workspace package.json files but apply and regenerate never restore them.

resolveCatalogProtocol() writes resolved versions into ALL workspace package.json files (root, packages/hydrogen, packages/cli, templates/skeleton, etc.). The validate command has a finally block (validate.ts:513-531) that restores these via git checkout, but neither apply nor regenerate has any such cleanup.

After running npx cookbook apply --recipe express, every workspace package.json with catalog: or workspace: references will be modified with inline versions. Same for regenerate — after the loop finishes, the workspace package.json files are left dirty. Only TEMPLATE_PATH gets restored via git checkout -- . and git clean -fd.

Additionally in regenerate, if resolveCatalogProtocol() itself throws, the existing git checkout/git clean lines (94-95) never run, so even the template directory is left dirty.

This is the same root problem as the scattered call sites comment below — the resolution step and cleanup are separated from the operation they support. I think the cleanest fix is to encapsulate the resolve-apply-cleanup lifecycle (see the design suggestion on validate.ts:428).


// Resolve in all workspace package.json files since npm traverses the workspace
const workspacePkgPaths: string[] = [path.join(REPO_ROOT, 'package.json')];
const workspaceYaml = YAML.parse(workspaceContent);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

non-blocking: double-parse of pnpm-workspace.yaml.

Line 591 already parses the workspace YAML into workspace, and this line parses the exact same workspaceContent again into workspaceYaml. These are identical objects from the same string.

I think this line should just reuse the already-parsed workspace:

Suggested change
const workspaceYaml = YAML.parse(workspaceContent);
const workspacePackages: string[] = workspace?.packages ?? [];

This also removes the confusing dual naming (workspace vs workspaceYaml for the same thing).

}

console.log('- 🔄 Resolving catalog: and workspace: protocol references…');
resolveCatalogProtocol();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

non-blocking: resolveCatalogProtocol() is called in 3 separate locations, requiring each caller to remember to call it before applyRecipe() and clean up after.

The knowledge that catalog/workspace protocols must be resolved before applying a recipe — and restored afterward — is scattered across:

  • validate.ts:428 (inside validateRecipe, with cleanup in finally)
  • commands/apply.ts:23 (before applyRecipe, no cleanup)
  • commands/regenerate.ts:75 (before applyRecipe in loop, no cleanup)

This is the same root problem as the blocking comment on apply.ts. Both the missing cleanup and the scattered call sites would be solved by encapsulating this inside applyRecipe() itself, or by introducing a withResolvedCatalog(fn) wrapper that handles resolve-execute-cleanup as a single unit. That way new callers can't forget to resolve or forget to clean up.

Not blocking since the current 3 call sites are correct (apart from the cleanup gap), but this is the design fix I'd recommend for the blocking issue.

typeof version === 'string' &&
version.startsWith('workspace:')
) {
const workspaceVersion = resolveWorkspaceVersion(name);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

non-blocking: workspace:^ and workspace:~ protocols are resolved to bare versions, losing the range prefix.

pnpm's workspace: protocol supports range specifiers:

  • workspace:* should resolve to exact version (e.g., 2026.1.0) — this works correctly
  • workspace:^ should resolve to ^2026.1.0
  • workspace:~ should resolve to ~2026.1.0

Currently all workspace: variants are resolved to the raw version from resolveWorkspaceVersion(). For the cookbook validation use case this might be fine since we just need npm install to succeed, but if any recipe package.json uses workspace:^, the resolved version would be more restrictive than intended.


for (const [name, version] of Object.entries(deps)) {
if (version === 'catalog:' || version === 'catalog:default') {
if (catalog[name]) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

non-blocking: missing catalog: entries are silently swallowed.

When a dependency references catalog:some-key but that key doesn't exist in pnpm-workspace.yaml's catalog, the if (catalog[name]) guard silently falls through without modifying the dependency. It keeps its unresolvable catalog:some-key value, and the subsequent npm install will fail with a confusing error rather than a clear message about the missing catalog entry.

I think a console.warn (or even an error) here would save debugging time — something like "Warning: catalog key 'some-key' not found in pnpm-workspace.yaml".

let _workspaceVersions: Map<string, string> | null = null;

/** @internal Reset cached workspace versions (for testing) */
export function _resetWorkspaceVersionsCache(): void {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: exporting _resetWorkspaceVersionsCache() from production code for testing.

The _ prefix convention signals this is internal, but it's still part of the public API surface. If this caching pattern grows, dependency injection (passing the cache or a reader function) would be more testable without exporting internals. Fine for now though.

* by looking up versions from pnpm-workspace.yaml catalog.
* This is needed because `npm install` doesn't understand the `catalog:` protocol.
*/
export function resolveCatalogProtocol(): void {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

non-blocking: resolveCatalogProtocol() lives in validate.ts but isn't validation.

This function resolves pnpm protocols so that npm can install — it's workspace setup, not validation. Having it in validate.ts means apply.ts and regenerate.ts both import from the validation module for something that has nothing to do with validation. If the cookbook grows, this would be better in its own module (e.g., workspace.ts) or co-located with apply.ts. Not urgent, but worth noting for future clarity.

status.modifiedFiles.filter(
(f) => !['package.json', 'package-lock.json'].includes(f),
(f) =>
!['package.json', 'package-lock.json'].includes(path.basename(f)),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

praise: nice catch on the path.basename fix.

The old code compared full paths like templates/skeleton/package.json against just package.json, which would never match for subdirectory files. path.basename(f) is the correct fix.

function installDependencies(): Command {
return {
command: 'npm install',
command: 'npm install --no-workspaces --ignore-scripts',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

praise: npm install --no-workspaces --ignore-scripts flags are a good addition.

--no-workspaces prevents npm from trying to process pnpm workspace configuration it doesn't understand, and --ignore-scripts avoids running lifecycle scripts in a validation-only context. Both are appropriate for this use case.

@@ -0,0 +1,201 @@
# Fix Cookbook Recipe Validation Failures
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

praise: the Claude command for fixing recipe validation is well-structured.

The 5-phase approach (Validate, Fix Patches, Fix Other Issues, Re-validate, Handle Multiple Recipes) with clear failure categories (A-E) is a thoughtful guide for automated recipe fixing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants