@thorprovider/storefront 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/README.md +333 -0
  3. package/assets/templates/app/app/[[...slug]]/page.tsx +17 -0
  4. package/assets/templates/app/app/[[...slug]]/renderer.tsx +10 -0
  5. package/assets/templates/app/app/globals.css +101 -0
  6. package/assets/templates/app/app/layout.tsx +35 -0
  7. package/assets/templates/app/lib/__STOREFRONT__/catalog.ts +88 -0
  8. package/assets/templates/app/lib/__STOREFRONT__/handlers.ts +42 -0
  9. package/assets/templates/app/lib/__STOREFRONT__/mappers.ts +47 -0
  10. package/assets/templates/app/lib/__STOREFRONT__/registry.tsx +98 -0
  11. package/assets/templates/app/lib/__STOREFRONT__/runtime.ts +17 -0
  12. package/assets/templates/app/lib/__STOREFRONT__/spec/index.ts +48 -0
  13. package/assets/templates/app/lib/__STOREFRONT__/spec/page-root.ts +26 -0
  14. package/assets/templates/app/lib/__STOREFRONT__/spec/types.ts +14 -0
  15. package/assets/templates/app/lib/__STOREFRONT__/state.ts +35 -0
  16. package/assets/templates/app/lib/__STOREFRONT__/views/index.ts +7 -0
  17. package/assets/templates/app/lib/__STOREFRONT__/views/sections/faq.tsx +29 -0
  18. package/assets/templates/app/lib/__STOREFRONT__/views/sections/logoCloud.tsx +29 -0
  19. package/assets/templates/app/lib/__STOREFRONT__/views/sections/pricing.tsx +33 -0
  20. package/assets/templates/app/lib/__STOREFRONT__/views/sections/statistics.tsx +29 -0
  21. package/assets/templates/app/lib/__STOREFRONT__/views/sections/testimonials.tsx +28 -0
  22. package/assets/templates/app/lib/__STOREFRONT__/views/types.ts +46 -0
  23. package/assets/templates/views/home/index.tsx +142 -0
  24. package/assets/templates/views/home/registry.tsx +65 -0
  25. package/bin/storefront.js +531 -0
  26. package/commands/sf-add-view.md +26 -0
  27. package/commands/sf-theme.md +20 -0
  28. package/commands/sf-view.md +25 -0
  29. package/package.json +48 -0
  30. package/recipes/archetype.schema.json +39 -0
  31. package/recipes/archetypes.json +148 -0
  32. package/recipes/recipe.schema.json +59 -0
  33. package/recipes/recipes.json +90 -0
  34. package/recipes/sections.json +46 -0
  35. package/recipes/validate.mjs +190 -0
  36. package/scripts/render-templates.mjs +255 -0
  37. package/scripts/sync-assets.mjs +87 -0
  38. package/skills/building-storefronts/SKILL.md +178 -0
  39. package/skills/building-storefronts/references/frontend-integration.md +229 -0
  40. package/skills/json-render-core/SKILL.md +291 -0
  41. package/skills/json-render-next/SKILL.md +194 -0
  42. package/skills/json-render-react/SKILL.md +298 -0
  43. package/skills/json-render-remotion/SKILL.md +111 -0
  44. package/skills/json-render-shadcn/SKILL.md +159 -0
  45. package/skills/json-render-solid/SKILL.md +204 -0
  46. package/skills/nextjs-shadcn/SKILL.md +367 -0
  47. package/skills/nextjs-shadcn/references/architecture.md +499 -0
  48. package/skills/nextjs-shadcn/references/project-setup.md +162 -0
  49. package/skills/nextjs-shadcn/references/shadcn-platform.md +261 -0
  50. package/skills/nextjs-shadcn/references/sidebar.md +274 -0
  51. package/skills/nextjs-shadcn/references/styling.md +555 -0
  52. package/skills/sf-theme-gen/SKILL.md +61 -0
  53. package/skills/sf-view-gen/SKILL.md +116 -0
  54. package/skills/sf-view-gen/assets/recipes/archetypes.json +148 -0
  55. package/skills/sf-view-gen/assets/recipes/recipes.json +90 -0
  56. package/skills/sf-view-gen/assets/recipes/sections.json +46 -0
  57. package/skills/sf-view-gen/assets/templates/views/category/index.tsx +86 -0
  58. package/skills/sf-view-gen/assets/templates/views/category/registry.tsx +48 -0
  59. package/skills/sf-view-gen/assets/templates/views/checkout/index.tsx +192 -0
  60. package/skills/sf-view-gen/assets/templates/views/checkout/registry.tsx +45 -0
  61. package/skills/sf-view-gen/assets/templates/views/product/index.tsx +122 -0
  62. package/skills/sf-view-gen/assets/templates/views/product/registry.tsx +49 -0
  63. package/skills/sf-view-gen/assets/templates/views/sections/faq.client.tsx +20 -0
  64. package/skills/sf-view-gen/assets/templates/views/sections/faq.tsx +21 -0
  65. package/skills/sf-view-gen/assets/templates/views/sections/logoCloud.client.tsx +18 -0
  66. package/skills/sf-view-gen/assets/templates/views/sections/logoCloud.tsx +21 -0
  67. package/skills/sf-view-gen/assets/templates/views/sections/pricing.client.tsx +20 -0
  68. package/skills/sf-view-gen/assets/templates/views/sections/pricing.tsx +23 -0
  69. package/skills/sf-view-gen/assets/templates/views/sections/statistics.client.tsx +18 -0
  70. package/skills/sf-view-gen/assets/templates/views/sections/statistics.tsx +21 -0
  71. package/skills/sf-view-gen/assets/templates/views/sections/testimonials.client.tsx +19 -0
  72. package/skills/sf-view-gen/assets/templates/views/sections/testimonials.tsx +19 -0
  73. package/skills/sf-view-gen/assets/templates/views/user-panel/index.tsx +168 -0
  74. package/skills/sf-view-gen/assets/templates/views/user-panel/registry.tsx +49 -0
  75. package/skills/sf-view-gen/references/views.md +69 -0
  76. package/skills/sf-view-gen/scripts/validate.mjs +190 -0
  77. package/skills/shadcn-component-discovery/SKILL.md +273 -0
  78. package/skills/shadcn-component-discovery/references/registries.md +226 -0
  79. package/skills/shadcn-theming/SKILL.md +104 -0
  80. package/skills/shadcn-theming/references/templates/theme-setup.md +109 -0
  81. package/skills/shadcn-theming/references/theming-guide.md +90 -0
  82. package/skills/storefront-best-practices/SKILL.md +421 -0
  83. package/skills/storefront-best-practices/reference/components/breadcrumbs.md +123 -0
  84. package/skills/storefront-best-practices/reference/components/cart-popup.md +189 -0
  85. package/skills/storefront-best-practices/reference/components/country-selector.md +298 -0
  86. package/skills/storefront-best-practices/reference/components/footer.md +112 -0
  87. package/skills/storefront-best-practices/reference/components/hero.md +241 -0
  88. package/skills/storefront-best-practices/reference/components/megamenu.md +239 -0
  89. package/skills/storefront-best-practices/reference/components/navbar.md +397 -0
  90. package/skills/storefront-best-practices/reference/components/popups.md +221 -0
  91. package/skills/storefront-best-practices/reference/components/product-card.md +125 -0
  92. package/skills/storefront-best-practices/reference/components/product-reviews.md +217 -0
  93. package/skills/storefront-best-practices/reference/components/product-slider.md +174 -0
  94. package/skills/storefront-best-practices/reference/components/search.md +101 -0
  95. package/skills/storefront-best-practices/reference/connecting-to-backend.md +391 -0
  96. package/skills/storefront-best-practices/reference/design.md +388 -0
  97. package/skills/storefront-best-practices/reference/features/promotions.md +307 -0
  98. package/skills/storefront-best-practices/reference/features/wishlist.md +230 -0
  99. package/skills/storefront-best-practices/reference/layouts/account.md +380 -0
  100. package/skills/storefront-best-practices/reference/layouts/cart.md +316 -0
  101. package/skills/storefront-best-practices/reference/layouts/checkout.md +486 -0
  102. package/skills/storefront-best-practices/reference/layouts/home-page.md +264 -0
  103. package/skills/storefront-best-practices/reference/layouts/order-confirmation.md +231 -0
  104. package/skills/storefront-best-practices/reference/layouts/product-details.md +527 -0
  105. package/skills/storefront-best-practices/reference/layouts/product-listing.md +520 -0
  106. package/skills/storefront-best-practices/reference/layouts/static-pages.md +356 -0
  107. package/skills/storefront-best-practices/reference/medusa.md +307 -0
  108. package/skills/storefront-best-practices/reference/mobile-responsiveness.md +183 -0
  109. package/skills/storefront-best-practices/reference/seo.md +195 -0
@@ -0,0 +1,255 @@
1
+ #!/usr/bin/env node
2
+ // Deterministic, network-free render of the scaffold templates into a target
3
+ // project. This is the single implementation of the copy stage used by both the
4
+ // `create` action (bin/storefront.js) and the dev harness below, so
5
+ // determinism tests and real scaffolds exercise the same code path.
6
+ //
7
+ // node scripts/render-templates.mjs <target-dir> [name]
8
+
9
+ import { mkdir, readdir, readFile, writeFile, rm } from 'node:fs/promises'
10
+ import { existsSync } from 'node:fs'
11
+ import path from 'node:path'
12
+ import { fileURLToPath } from 'node:url'
13
+
14
+ const here = path.dirname(fileURLToPath(import.meta.url))
15
+ const pkgRoot = path.resolve(here, '..')
16
+ export const templatesRoot = path.join(pkgRoot, 'assets', 'templates')
17
+
18
+ export const HEADER_RE = /^\/\/ Generated by[\s\S]*?\(template: ([^)]+)\)/
19
+
20
+ export function toKebab(value) {
21
+ return value
22
+ .trim()
23
+ .toLowerCase()
24
+ .replace(/[^a-z0-9]+/g, '-')
25
+ .replace(/^-+|-+$/g, '')
26
+ }
27
+
28
+ export function toPascal(value) {
29
+ return value.replace(/(^\w|[-_]\w)/g, (m) => m.replace(/[-_]/, '').toUpperCase())
30
+ }
31
+
32
+ export function replacePlaceholders(content, { name, siteName }) {
33
+ const camel = toPascal(name)
34
+ const header = content.match(HEADER_RE)?.[1]
35
+ let body = content
36
+ if (header) body = content.replace(HEADER_RE, '@@HEADER@@')
37
+ body = body
38
+ .replaceAll('__STOREFRONT_CAMEL__', camel)
39
+ .replaceAll('__STOREFRONT__', name)
40
+ .replaceAll('__SITE_NAME__', siteName)
41
+ if (header) {
42
+ body = body.replace('@@HEADER@@', `// Generated by Thor Commerce storefront (template: ${header})`)
43
+ }
44
+ return body
45
+ }
46
+
47
+ async function walk(root, dir, out) {
48
+ for (const entry of await readdir(dir, { withFileTypes: true })) {
49
+ const full = path.join(dir, entry.name)
50
+ if (entry.isDirectory()) await walk(root, full, out)
51
+ else out.push({ abs: full, rel: path.relative(root, full) })
52
+ }
53
+ return out
54
+ }
55
+
56
+ export async function renderTree(fromDir, toDir, subs) {
57
+ await mkdir(toDir, { recursive: true })
58
+ for (const { abs, rel } of await walk(fromDir, fromDir, [])) {
59
+ const dest = path.join(toDir, rel).replaceAll('__STOREFRONT__', subs.name)
60
+ await mkdir(path.dirname(dest), { recursive: true })
61
+ const raw = await readFile(abs, 'utf8')
62
+ await writeFile(dest, replacePlaceholders(raw, subs))
63
+ }
64
+ }
65
+
66
+ export async function writeBarrel(viewsDir) {
67
+ const viewNames = (await readdir(viewsDir, { withFileTypes: true }))
68
+ .filter((e) => e.isDirectory() && existsSync(path.join(viewsDir, e.name, 'index.tsx')))
69
+ .map((e) => e.name)
70
+ .sort()
71
+ const body =
72
+ `// Generated by Thor Commerce storefront (template: assets/templates/app/lib/__STOREFRONT__/views/index.ts)\n` +
73
+ `// View registry (barrel). A freshly scaffolded store registers only \`home\`.\n` +
74
+ `// Adding a view appends one import and one entry here; shared files never change.\n\n` +
75
+ `import type { ViewModule } from './types'\n` +
76
+ viewNames.map((v) => `import { ${v} } from './${v}'`).join('\n') +
77
+ `\n\nexport const views: ViewModule[] = [${viewNames.join(', ')}]\n`
78
+ await writeFile(path.join(viewsDir, 'index.ts'), body)
79
+ }
80
+
81
+ // The storefront's required markers inside app/layout.tsx and app/globals.css.
82
+ // These files belong to create-next-app + shadcn: the scaffold never deletes or
83
+ // rewrites them wholesale — it only appends these markers when they are missing,
84
+ // so user edits elsewhere in the files survive.
85
+ const LAYOUT_MARKER = '@thorprovider/runtime/host-glue/providers'
86
+ // Pre-rename marker: `create --force` over a project scaffolded with the old
87
+ // scaffolder must still detect the injection as present (cross-version
88
+ // idempotency) instead of duplicating the provider block.
89
+ const LEGACY_LAYOUT_MARKER = '@thorprovider/storefronts/host-glue/providers'
90
+ const GLOBALS_IMPORT = '@import "@thorprovider/blocks/styles.css";'
91
+ const GLOBALS_TOKENS_MARKER = '--success:'
92
+
93
+ // The block token contract the scaffold guarantees on top of whatever the shadcn
94
+ // preset produced. Values mirror assets/templates/app/app/globals.css (success /
95
+ // warning AA-conformant pairs in both modes); only these tokens are added, never
96
+ // a re-statement of the preset's own tokens.
97
+ const BLOCK_TOKEN_APPEND = `@theme inline {
98
+ --color-success: var(--success);
99
+ --color-success-foreground: var(--success-foreground);
100
+ --color-warning: var(--warning);
101
+ --color-warning-foreground: var(--warning-foreground);
102
+ }
103
+
104
+ :root {
105
+ --success: oklch(0.47 0.14 155);
106
+ --success-foreground: oklch(0.98 0.01 155);
107
+ --warning: oklch(0.55 0.18 75);
108
+ --warning-foreground: oklch(0.98 0.02 75);
109
+ }
110
+
111
+ .dark {
112
+ --success: oklch(0.6 0.13 155);
113
+ --success-foreground: oklch(0.1 0.02 155);
114
+ --warning: oklch(0.7 0.15 75);
115
+ --warning-foreground: oklch(0.1 0.02 75);
116
+ }
117
+ `
118
+
119
+ /**
120
+ * Idempotently inject the storefront provider wiring into an existing
121
+ * app/layout.tsx (the create-next-app default). Adds the imports and themeCss
122
+ * const after the last import line, then wraps {children} in the provider.
123
+ * Returns the content unchanged when the provider marker is already present
124
+ * (so a re-run never duplicates the injection and user edits survive).
125
+ */
126
+ export function injectLayout(content, name) {
127
+ if (content.includes(LAYOUT_MARKER) || content.includes(LEGACY_LAYOUT_MARKER)) return content
128
+
129
+ const importLines = [
130
+ `import { StorefrontProvider } from '${LAYOUT_MARKER}'`,
131
+ `import { themeVarsToCss, themeTokensToVars } from '@thorprovider/runtime/render-core/theme'`,
132
+ `import { DEFAULT_THEME_TOKENS } from '@thorprovider/runtime/contracts'`,
133
+ `import { registry } from '@/lib/${name}/registry'`,
134
+ ]
135
+ const constLine = 'const themeCss = themeVarsToCss(themeTokensToVars(DEFAULT_THEME_TOKENS))'
136
+
137
+ const lines = content.split('\n')
138
+ let lastImport = -1
139
+ for (let i = 0; i < lines.length; i++) {
140
+ if (lines[i].trimStart().startsWith('import ')) lastImport = i
141
+ }
142
+ let out
143
+ if (lastImport >= 0) {
144
+ lines.splice(lastImport + 1, 0, '', ...importLines, '', constLine)
145
+ out = lines.join('\n')
146
+ } else {
147
+ out = [...importLines, '', constLine, '', content].join('\n')
148
+ }
149
+
150
+ if (!/\{children\}/.test(out)) return out
151
+ out = out.replace(
152
+ /\{children\}/,
153
+ '<StorefrontProvider themeCss={themeCss} registry={registry}>{children}</StorefrontProvider>',
154
+ )
155
+ return out
156
+ }
157
+
158
+ /**
159
+ * Idempotently append the blocks stylesheet import and the block token contract
160
+ * to an existing app/globals.css (produced by create-next-app + shadcn init).
161
+ * The @import is inserted after the last leading @import line; the success /
162
+ * warning tokens are appended at the end. Returns the content unchanged when
163
+ * the blocks import is already present and the success tokens exist.
164
+ */
165
+ export function injectGlobals(content) {
166
+ const lines = content.split('\n')
167
+ let lastImport = -1
168
+ for (let i = 0; i < lines.length; i++) {
169
+ if (lines[i].trimStart().startsWith('@import ')) lastImport = i
170
+ }
171
+ let out
172
+ if (content.includes(GLOBALS_IMPORT)) {
173
+ out = content
174
+ } else if (lastImport >= 0) {
175
+ lines.splice(lastImport + 1, 0, GLOBALS_IMPORT)
176
+ out = lines.join('\n')
177
+ } else {
178
+ out = `${GLOBALS_IMPORT}\n${content}`
179
+ }
180
+
181
+ if (out.includes(GLOBALS_TOKENS_MARKER)) return out
182
+ return `${out.replace(/\s*$/, '')}\n\n${BLOCK_TOKEN_APPEND}`
183
+ }
184
+
185
+ /**
186
+ * Render the owned surfaces into `targetDir` — the catch-all route
187
+ * `app/[[...slug]]/` and `lib/<name>/` — then register the views in the barrel.
188
+ * `app/layout.tsx` and `app/globals.css` are NOT owned: when they exist they are
189
+ * only appended to (idempotently); when absent they are written from the
190
+ * template so a fresh project still ships the provider and block tokens.
191
+ * Removes the template-owned surfaces (and the create-next-app default page)
192
+ * first so repeated runs stay clean and no stage deletes another's output.
193
+ */
194
+ export async function renderScaffold({ targetDir, name, siteName }) {
195
+ const viewsDir = path.join(targetDir, 'lib', name, 'views')
196
+ await rm(path.join(targetDir, 'lib', name), { recursive: true, force: true })
197
+ await rm(path.join(targetDir, 'app', '[[...slug]]'), { recursive: true, force: true })
198
+ await rm(path.join(targetDir, 'app', 'page.tsx'), { force: true })
199
+
200
+ await renderTree(path.join(templatesRoot, 'app', 'app', '[[...slug]]'), path.join(targetDir, 'app', '[[...slug]]'), { name, siteName })
201
+ await renderTree(path.join(templatesRoot, 'app', 'lib'), path.join(targetDir, 'lib'), { name, siteName })
202
+ await renderTree(path.join(templatesRoot, 'views'), viewsDir, { name, siteName })
203
+ await writeBarrel(viewsDir)
204
+
205
+ await ensureLayoutAppend(targetDir, name, siteName)
206
+ await ensureGlobalsAppend(targetDir, name, siteName)
207
+ }
208
+
209
+ async function ensureLayoutAppend(targetDir, name, siteName) {
210
+ const layoutPath = path.join(targetDir, 'app', 'layout.tsx')
211
+ const templatePath = path.join(templatesRoot, 'app', 'app', 'layout.tsx')
212
+ if (!existsSync(layoutPath)) {
213
+ const raw = await readFile(templatePath, 'utf8')
214
+ await writeFile(layoutPath, replacePlaceholders(raw, { name, siteName }))
215
+ return
216
+ }
217
+ const existing = await readFile(layoutPath, 'utf8')
218
+ const injected = injectLayout(existing, name)
219
+ if (injected !== existing) await writeFile(layoutPath, injected)
220
+ }
221
+
222
+ async function ensureGlobalsAppend(targetDir, name, siteName) {
223
+ const globalsPath = path.join(targetDir, 'app', 'globals.css')
224
+ const templatePath = path.join(templatesRoot, 'app', 'app', 'globals.css')
225
+ if (!existsSync(globalsPath)) {
226
+ const raw = await readFile(templatePath, 'utf8')
227
+ await writeFile(globalsPath, replacePlaceholders(raw, { name, siteName }))
228
+ return
229
+ }
230
+ const existing = await readFile(globalsPath, 'utf8')
231
+ const injected = injectGlobals(existing)
232
+ if (injected !== existing) await writeFile(globalsPath, injected)
233
+ }
234
+
235
+ const invokedDirectly = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)
236
+ if (invokedDirectly) {
237
+ const targetArg = process.argv[2]
238
+ if (!targetArg) {
239
+ console.error('usage: node scripts/render-templates.mjs <target-dir> [name]')
240
+ process.exit(1)
241
+ }
242
+ const target = path.resolve(process.cwd(), targetArg)
243
+ const name = toKebab(process.argv[3] ?? path.basename(target))
244
+ const siteName = name
245
+ if (!existsSync(templatesRoot)) {
246
+ console.error(`templates not found: ${templatesRoot}`)
247
+ process.exit(1)
248
+ }
249
+ if (!existsSync(target)) {
250
+ console.error(`target does not exist: ${target} (create the Next.js project first)`)
251
+ process.exit(1)
252
+ }
253
+ await renderScaffold({ targetDir: target, name, siteName })
254
+ console.log(`rendered templates for "${name}" (camel: ${toPascal(name)}) into ${target}`)
255
+ }
@@ -0,0 +1,87 @@
1
+ #!/usr/bin/env node
2
+ // Keeps the per-skill copies of shared assets in sync with the single source.
3
+ //
4
+ // Source of truth: recipes/ at the package root.
5
+ // Generated copies:
6
+ // skills/sf-view-gen/assets/recipes/{recipes,archetypes,sections}.json
7
+ // skills/sf-view-gen/scripts/validate.mjs
8
+ //
9
+ // Usage:
10
+ // node scripts/sync-assets.mjs # write the copies from source
11
+ // node scripts/sync-assets.mjs --check # report divergence, exit 1 if any
12
+
13
+ import { createHash } from 'node:crypto'
14
+ import { existsSync } from 'node:fs'
15
+ import { mkdir, readFile, writeFile } from 'node:fs/promises'
16
+ import { dirname, join, resolve } from 'node:path'
17
+ import { fileURLToPath } from 'node:url'
18
+
19
+ const __dirname = dirname(fileURLToPath(import.meta.url))
20
+ const PKG_ROOT = resolve(__dirname, '..')
21
+
22
+ const SOURCE = join(PKG_ROOT, 'recipes')
23
+
24
+ const MANIFEST = [
25
+ ['recipes.json', join(PKG_ROOT, 'skills/sf-view-gen/assets/recipes/recipes.json')],
26
+ ['archetypes.json', join(PKG_ROOT, 'skills/sf-view-gen/assets/recipes/archetypes.json')],
27
+ ['sections.json', join(PKG_ROOT, 'skills/sf-view-gen/assets/recipes/sections.json')],
28
+ ['validate.mjs', join(PKG_ROOT, 'skills/sf-view-gen/scripts/validate.mjs')],
29
+ ]
30
+
31
+ function sha256(text) {
32
+ return createHash('sha256').update(text).digest('hex')
33
+ }
34
+
35
+ export async function checkAssets() {
36
+ const diverged = []
37
+ for (const [sourceName, destPath] of MANIFEST) {
38
+ const source = await readFile(join(SOURCE, sourceName), 'utf8')
39
+ if (!existsSync(destPath)) {
40
+ diverged.push(`${destPath} (missing)`)
41
+ continue
42
+ }
43
+ const copy = await readFile(destPath, 'utf8')
44
+ if (sha256(source) !== sha256(copy)) {
45
+ diverged.push(destPath)
46
+ }
47
+ }
48
+ return diverged
49
+ }
50
+
51
+ async function syncAssets() {
52
+ let written = 0
53
+ for (const [sourceName, destPath] of MANIFEST) {
54
+ const source = await readFile(join(SOURCE, sourceName), 'utf8')
55
+ await mkdir(dirname(destPath), { recursive: true })
56
+ await writeFile(destPath, source, 'utf8')
57
+ written++
58
+ console.log(` synced ${destPath}`)
59
+ }
60
+ return written
61
+ }
62
+
63
+ async function main() {
64
+ const check = process.argv.includes('--check')
65
+ if (check) {
66
+ const diverged = await checkAssets()
67
+ if (diverged.length > 0) {
68
+ console.error('Generated assets are out of sync with recipes/:')
69
+ for (const file of diverged) console.error(` ${file}`)
70
+ console.error('Run `node scripts/sync-assets.mjs` to regenerate.')
71
+ process.exitCode = 1
72
+ return
73
+ }
74
+ console.log('Generated assets are in sync with recipes/.')
75
+ return
76
+ }
77
+
78
+ const written = await syncAssets()
79
+ console.log(`Synced ${written} generated asset(s) from recipes/.`)
80
+ }
81
+
82
+ if (process.argv[1] && resolve(process.argv[1]) === resolve(fileURLToPath(import.meta.url))) {
83
+ main().catch((err) => {
84
+ console.error(err)
85
+ process.exitCode = 1
86
+ })
87
+ }
@@ -0,0 +1,178 @@
1
+ ---
2
+ name: building-storefronts
3
+ description: Load automatically when planning, researching, or implementing Medusa storefront features (calling custom API routes, SDK integration, React Query patterns, data fetching). REQUIRED for all storefront development in ALL modes (planning, implementation, exploration). Contains SDK usage patterns, frontend integration, and critical rules for calling Medusa APIs.
4
+ ---
5
+
6
+ # Medusa Storefront Development
7
+
8
+ Frontend integration guide for building storefronts with Medusa. Covers SDK usage, React Query patterns, and calling custom API routes.
9
+
10
+ ## When to Apply
11
+
12
+ **Load this skill for ANY storefront development task, including:**
13
+ - Calling custom Medusa API routes from the storefront
14
+ - Integrating Medusa SDK in frontend applications
15
+ - Using React Query for data fetching
16
+ - Implementing mutations with optimistic updates
17
+ - Error handling and cache invalidation
18
+
19
+ **Also load building-with-medusa when:** Building the backend API routes that the storefront calls
20
+
21
+ ## CRITICAL: Load Reference Files When Needed
22
+
23
+ **The quick reference below is NOT sufficient for implementation.** You MUST load the reference file before writing storefront integration code.
24
+
25
+ **Load this reference when implementing storefront features:**
26
+
27
+ - **Calling API routes?** → MUST load `references/frontend-integration.md` first
28
+ - **Using SDK?** → MUST load `references/frontend-integration.md` first
29
+ - **Implementing React Query?** → MUST load `references/frontend-integration.md` first
30
+
31
+ ## Rule Categories by Priority
32
+
33
+ | Priority | Category | Impact | Prefix |
34
+ |----------|----------|--------|--------|
35
+ | 1 | SDK Usage | CRITICAL | `sdk-` |
36
+ | 2 | React Query Patterns | HIGH | `query-` |
37
+ | 3 | Data Display | HIGH (includes CRITICAL price rule) | `display-` |
38
+ | 4 | Error Handling | MEDIUM | `error-` |
39
+
40
+ ## Quick Reference
41
+
42
+ ### 1. SDK Usage (CRITICAL)
43
+
44
+ - `sdk-always-use` - **ALWAYS use the Medusa JS SDK for ALL API requests** - NEVER use regular fetch()
45
+ - `sdk-existing-methods` - For built-in endpoints, use existing SDK methods (`sdk.store.product.list()`, `sdk.admin.order.retrieve()`)
46
+ - `sdk-client-fetch` - For custom API routes, use `sdk.client.fetch()`
47
+ - `sdk-required-headers` - SDK automatically adds required headers (publishable API key for store, auth for admin) - regular fetch() missing these headers causes errors
48
+ - `sdk-no-json-stringify` - **NEVER use JSON.stringify() on body** - SDK handles serialization automatically
49
+ - `sdk-plain-objects` - Pass plain JavaScript objects to body, not strings
50
+ - `sdk-locate-first` - Always locate where SDK is instantiated in the project before using it
51
+
52
+ ### 2. React Query Patterns (HIGH)
53
+
54
+ - `query-use-query` - Use `useQuery` for GET requests (data fetching)
55
+ - `query-use-mutation` - Use `useMutation` for POST/DELETE requests (mutations)
56
+ - `query-invalidate` - Invalidate queries in `onSuccess` to refresh data after mutations
57
+ - `query-keys-hierarchical` - Structure query keys hierarchically for effective cache management
58
+ - `query-loading-states` - Always handle `isLoading`, `isPending`, `isError` states
59
+
60
+ ### 3. Data Display (HIGH)
61
+
62
+ - `display-price-format` - **CRITICAL**: Prices from Medusa are stored as-is ($49.99 = 49.99, NOT in cents). Display them directly - NEVER divide by 100
63
+
64
+ ### 4. Error Handling (MEDIUM)
65
+
66
+ - `error-on-error` - Implement `onError` callback in mutations to handle failures
67
+ - `error-display` - Show error messages to users when mutations fail
68
+ - `error-rollback` - Use optimistic updates with rollback on error for better UX
69
+
70
+ ## Critical SDK Pattern
71
+
72
+ **ALWAYS pass plain objects to the SDK - NEVER use JSON.stringify():**
73
+
74
+ ```typescript
75
+ // ✅ CORRECT - Plain object
76
+ await sdk.client.fetch("/store/reviews", {
77
+ method: "POST",
78
+ body: {
79
+ product_id: "prod_123",
80
+ rating: 5,
81
+ }
82
+ })
83
+
84
+ // ❌ WRONG - JSON.stringify breaks the request
85
+ await sdk.client.fetch("/store/reviews", {
86
+ method: "POST",
87
+ body: JSON.stringify({ // ❌ DON'T DO THIS!
88
+ product_id: "prod_123",
89
+ rating: 5,
90
+ })
91
+ })
92
+ ```
93
+
94
+ **Why this matters:**
95
+ - The SDK handles JSON serialization automatically
96
+ - Using JSON.stringify() will double-serialize and break the request
97
+ - The server won't be able to parse the body
98
+
99
+ ## Common Mistakes Checklist
100
+
101
+ Before implementing, verify you're NOT doing these:
102
+
103
+ **SDK Usage:**
104
+ - [ ] Using regular fetch() instead of the Medusa JS SDK (causes missing header errors)
105
+ - [ ] Not using existing SDK methods for built-in endpoints (e.g., using sdk.client.fetch("/store/products") instead of sdk.store.product.list())
106
+ - [ ] Using JSON.stringify() on the body parameter
107
+ - [ ] Manually setting Content-Type headers (SDK adds them)
108
+ - [ ] Hardcoding SDK import paths (locate in project first)
109
+ - [ ] Not using sdk.client.fetch() for custom routes
110
+
111
+ **React Query:**
112
+ - [ ] Not invalidating queries after mutations
113
+ - [ ] Using flat query keys instead of hierarchical
114
+ - [ ] Not handling loading and error states
115
+ - [ ] Forgetting to disable buttons during mutations (isPending)
116
+
117
+ **Data Display:**
118
+ - [ ] **CRITICAL**: Dividing prices by 100 when displaying (prices are stored as-is: $49.99 = 49.99, NOT in cents)
119
+
120
+ **Error Handling:**
121
+ - [ ] Not implementing onError callbacks
122
+ - [ ] Not showing error messages to users
123
+ - [ ] Not handling network failures gracefully
124
+
125
+ ## How to Use
126
+
127
+ **For detailed patterns and examples, load reference file:**
128
+
129
+ ```
130
+ references/frontend-integration.md - SDK usage, React Query patterns, API integration
131
+ ```
132
+
133
+ The reference file contains:
134
+ - Step-by-step SDK integration patterns
135
+ - Complete React Query examples
136
+ - Correct vs incorrect code examples
137
+ - Query key best practices
138
+ - Optimistic update patterns
139
+ - Error handling strategies
140
+
141
+ ## When to Use MedusaDocs MCP Server
142
+
143
+ **Use this skill for (PRIMARY SOURCE):**
144
+ - How to call custom API routes from storefront
145
+ - SDK usage patterns (sdk.client.fetch)
146
+ - React Query integration patterns
147
+ - Common mistakes and anti-patterns
148
+
149
+ **Use MedusaDocs MCP server for (SECONDARY SOURCE):**
150
+ - Built-in SDK methods (sdk.admin.*, sdk.store.*)
151
+ - Official Medusa SDK API reference
152
+ - Framework-specific configuration options
153
+
154
+ **Why skills come first:**
155
+ - Skills contain critical patterns like "don't use JSON.stringify" that MCP doesn't emphasize
156
+ - Skills show correct vs incorrect patterns; MCP shows what's possible
157
+ - Planning requires understanding patterns, not just API reference
158
+
159
+ ## Integration with Backend
160
+
161
+ **⚠️ CRITICAL: ALWAYS use the Medusa JS SDK - NEVER use regular fetch()**
162
+
163
+ When building features that span backend and frontend:
164
+
165
+ 1. **Backend (building-with-medusa skill):** Module → Workflow → API Route
166
+ 2. **Storefront (this skill):** SDK → React Query → UI Components
167
+ 3. **Connection:**
168
+ - Built-in endpoints: Use existing SDK methods (`sdk.store.product.list()`)
169
+ - Custom API routes: Use `sdk.client.fetch("/store/my-route")`
170
+ - **NEVER use regular fetch()** - missing publishable API key causes errors
171
+
172
+ **Why the SDK is required:**
173
+ - Store routes need `x-publishable-api-key` header
174
+ - Admin routes need `Authorization` and session headers
175
+ - SDK handles all required headers automatically
176
+ - Regular fetch() without headers → authentication/authorization errors
177
+
178
+ See `building-with-medusa` for backend API route patterns.