-
Notifications
You must be signed in to change notification settings - Fork 2.7k
feat: support external recipes in cookbook #831
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
ba9567c
de75aec
fd24ef8
04083bd
0273497
7168380
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -742,9 +742,12 @@ function generateSamplesData() { | |
| const allTags = new Set(); | ||
| let totalRecipes = 0; | ||
|
|
||
| const cookbooks = cookbookManifest.cookbooks.map((cookbook) => { | ||
| // Collect languages | ||
| // First pass: collect all known language IDs across cookbooks | ||
| cookbookManifest.cookbooks.forEach((cookbook) => { | ||
| cookbook.languages.forEach((lang) => allLanguages.add(lang.id)); | ||
| }); | ||
|
|
||
| const cookbooks = cookbookManifest.cookbooks.map((cookbook) => { | ||
|
|
||
| // Process recipes and add file paths | ||
| const recipes = cookbook.recipes.map((recipe) => { | ||
|
|
@@ -753,6 +756,36 @@ function generateSamplesData() { | |
| recipe.tags.forEach((tag) => allTags.add(tag)); | ||
| } | ||
|
|
||
| totalRecipes++; | ||
|
|
||
| // External recipes link to an external URL — skip local file resolution | ||
| if (recipe.external) { | ||
| if (recipe.url) { | ||
| try { | ||
| new URL(recipe.url); | ||
| } catch { | ||
| console.warn(`Warning: Invalid URL for external recipe "${recipe.id}": ${recipe.url}`); | ||
| } | ||
| } else { | ||
| console.warn(`Warning: External recipe "${recipe.id}" is missing a url`); | ||
| } | ||
|
|
||
| // Derive languages from tags that match known language IDs | ||
| const recipeLanguages = (recipe.tags || []).filter((tag) => allLanguages.has(tag)); | ||
|
|
||
| return { | ||
| id: recipe.id, | ||
| name: recipe.name, | ||
| description: recipe.description, | ||
| tags: recipe.tags || [], | ||
| languages: recipeLanguages, | ||
| external: true, | ||
| url: recipe.url || null, | ||
| author: recipe.author || null, | ||
| variants: {}, | ||
| }; | ||
| } | ||
|
Comment on lines
+762
to
+787
|
||
|
|
||
| // Build variants with file paths for each language | ||
| const variants = {}; | ||
| cookbook.languages.forEach((lang) => { | ||
|
|
@@ -771,13 +804,12 @@ function generateSamplesData() { | |
| } | ||
| }); | ||
|
|
||
| totalRecipes++; | ||
|
|
||
| return { | ||
| id: recipe.id, | ||
| name: recipe.name, | ||
| description: recipe.description, | ||
| tags: recipe.tags || [], | ||
| languages: Object.keys(variants), | ||
| variants, | ||
| }; | ||
| }); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -25,7 +25,11 @@ interface Recipe { | |
| name: string; | ||
| description: string; | ||
| tags: string[]; | ||
| languages: string[]; | ||
| variants: Record<string, RecipeVariant>; | ||
| external?: boolean; | ||
| url?: string | null; | ||
| author?: { name: string; url?: string } | null; | ||
| } | ||
|
|
||
| interface Cookbook { | ||
|
|
@@ -138,7 +142,7 @@ function setupFilters(): void { | |
| languages.forEach((lang, id) => { | ||
| const option = document.createElement("option"); | ||
| option.value = id; | ||
| option.textContent = `${lang.icon} ${lang.name}`; | ||
| option.textContent = lang.name; | ||
| languageSelect.appendChild(option); | ||
| }); | ||
|
|
||
|
|
@@ -257,10 +261,10 @@ function getFilteredRecipes(): { | |
| ); | ||
| } | ||
|
|
||
| // Apply language filter | ||
| // Apply language filter using per-recipe languages array | ||
| if (selectedLanguage) { | ||
| results = results.filter( | ||
| ({ recipe }) => recipe.variants[selectedLanguage!] | ||
| results = results.filter(({ recipe }) => | ||
| recipe.languages.includes(selectedLanguage!) | ||
| ); | ||
| } | ||
|
|
||
|
|
@@ -370,15 +374,55 @@ function renderRecipeCard( | |
| const recipeKey = `${cookbook.id}-${recipe.id}`; | ||
| const isExpanded = expandedRecipes.has(recipeKey); | ||
|
|
||
| // Determine which language to show | ||
| const displayLang = selectedLanguage || cookbook.languages[0]?.id || "nodejs"; | ||
| const variant = recipe.variants[displayLang]; | ||
|
|
||
| const tags = recipe.tags | ||
| .map((tag) => `<span class="recipe-tag">${escapeHtml(tag)}</span>`) | ||
| .join(""); | ||
|
|
||
| const langIndicators = cookbook.languages | ||
| // External recipe — link to external URL | ||
| if (recipe.external && recipe.url) { | ||
| const authorHtml = recipe.author | ||
| ? `<span class="recipe-author">by ${ | ||
| recipe.author.url | ||
| ? `<a href="${escapeHtml(recipe.author.url)}" target="_blank" rel="noopener">${escapeHtml(recipe.author.name)}</a>` | ||
| : escapeHtml(recipe.author.name) | ||
| }</span>` | ||
| : ""; | ||
|
|
||
| return ` | ||
| <div class="recipe-card external${ | ||
| isExpanded ? " expanded" : "" | ||
| }" data-recipe="${escapeHtml(recipeKey)}"> | ||
| <div class="recipe-header"> | ||
| <h3>${highlightedName || escapeHtml(recipe.name)}</h3> | ||
| <span class="recipe-badge external-badge" title="External project"> | ||
| <svg viewBox="0 0 16 16" width="14" height="14" fill="currentColor" aria-hidden="true"> | ||
| <path d="M3.75 2h3.5a.75.75 0 0 1 0 1.5h-3.5a.25.25 0 0 0-.25.25v8.5c0 .138.112.25.25.25h8.5a.25.25 0 0 0 .25-.25v-3.5a.75.75 0 0 1 1.5 0v3.5A1.75 1.75 0 0 1 12.25 14h-8.5A1.75 1.75 0 0 1 2 12.25v-8.5C2 2.784 2.784 2 3.75 2Zm6.854-1h4.146a.25.25 0 0 1 .25.25v4.146a.25.25 0 0 1-.427.177L13.03 4.03 9.28 7.78a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042l3.75-3.75-1.543-1.543A.25.25 0 0 1 10.604 1Z"/> | ||
| </svg> | ||
| Community | ||
| </span> | ||
| </div> | ||
| <p class="recipe-description">${escapeHtml(recipe.description)}</p> | ||
| ${authorHtml ? `<div class="recipe-author-line">${authorHtml}</div>` : ""} | ||
| <div class="recipe-tags">${tags}</div> | ||
| <div class="recipe-actions"> | ||
| <a href="${escapeHtml(recipe.url)}" | ||
| class="btn btn-primary btn-small" target="_blank" rel="noopener"> | ||
| <svg viewBox="0 0 16 16" width="14" height="14" fill="currentColor" aria-hidden="true"> | ||
| <path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/> | ||
| </svg> | ||
| View on GitHub | ||
|
||
| </a> | ||
| </div> | ||
| </div> | ||
| `; | ||
| } | ||
|
|
||
| // Local recipe — existing behavior | ||
| // Determine which language to show | ||
| const displayLang = selectedLanguage || cookbook.languages?.[0]?.id || "nodejs"; | ||
| const variant = recipe.variants[displayLang]; | ||
|
|
||
| const langIndicators = (cookbook.languages ?? []) | ||
| .filter((lang) => recipe.variants[lang.id]) | ||
| .map( | ||
| (lang) => | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The code assumes
cookbook.languagesalways exists and is an array, but doesn't handle the case where it might be undefined or null. This could cause a runtime error when processing cookbooks with missing or invalid language configuration. Consider adding a null check or defaulting to an empty array.This issue also appears on line 791 of the same file.