@thorprovider/storefront 0.4.0-mvp.0 → 0.4.0-mvp.2
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.
- package/README.md +173 -153
- package/VENDOR.md +69 -0
- package/assets/openspec/config.yaml +38 -0
- package/assets/templates/AGENTS.md +76 -0
- package/assets/templates/app/app/[[...slug]]/page.tsx +42 -1
- package/assets/templates/app/app/[[...slug]]/renderer.tsx +13 -2
- package/assets/templates/app/app/globals.css +5 -8
- package/assets/templates/app/app/layout.tsx +1 -5
- package/assets/templates/app/app/robots.ts +9 -3
- package/assets/templates/app/app/sitemap.ts +64 -5
- package/assets/templates/app/lib/__STOREFRONT__/actions.ts +70 -0
- package/assets/templates/app/lib/__STOREFRONT__/catalog.ts +15 -5
- package/assets/templates/app/lib/__STOREFRONT__/commerce.ts +71 -0
- package/assets/templates/app/lib/__STOREFRONT__/composition.ts +26 -0
- package/assets/templates/app/lib/__STOREFRONT__/handlers.core.ts +36 -4
- package/assets/templates/app/lib/__STOREFRONT__/registry.tsx +82 -50
- package/assets/templates/app/lib/__STOREFRONT__/runtime.ts +3 -0
- package/assets/templates/app/lib/__STOREFRONT__/schema.ts +29 -14
- package/assets/templates/app/lib/__STOREFRONT__/section-observation.ts +182 -0
- package/assets/templates/app/lib/__STOREFRONT__/section-sources.ts +130 -0
- package/assets/templates/app/lib/__STOREFRONT__/shell/content.ts +31 -7
- package/assets/templates/app/lib/__STOREFRONT__/shell/props.ts +4 -0
- package/assets/templates/app/lib/__STOREFRONT__/spec/index.ts +56 -26
- package/assets/templates/app/lib/__STOREFRONT__/spec/seo.ts +165 -0
- package/assets/templates/app/lib/__STOREFRONT__/store-context.ts +26 -7
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/index.ts +2 -1
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/products.client.tsx +100 -0
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/products.tsx +19 -0
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/schema.products.ts +39 -0
- package/assets/templates/features/checkout/handlers.ts +6 -0
- package/assets/templates/features/customer-auth/state.ts +10 -1
- package/assets/templates/views/account/index.tsx +1 -1
- package/assets/templates/views/account/registry.tsx +80 -19
- package/assets/templates/views/account/schema.ts +1 -0
- package/assets/templates/views/cart/content.ts +11 -0
- package/assets/templates/views/cart/data.ts +23 -0
- package/assets/templates/views/cart/index.tsx +53 -0
- package/assets/templates/views/cart/props.ts +12 -0
- package/assets/templates/views/cart/registry.tsx +81 -0
- package/assets/templates/views/cart/schema.ts +52 -0
- package/assets/templates/views/category/assembly/assembly.json +1 -0
- package/assets/templates/views/category/assembly/index.skeleton.tsx +64 -6
- package/assets/templates/views/category/assembly/registry.skeleton.tsx +2 -1
- package/assets/templates/views/category/assembly/slots/breadcrumb/Breadcrumb/binding.json +5 -0
- package/assets/templates/views/category/assembly/slots/breadcrumb/Breadcrumb/catalog.part +4 -0
- package/assets/templates/views/category/assembly/slots/breadcrumb/Breadcrumb/element.part +7 -0
- package/assets/templates/views/category/assembly/slots/breadcrumb/Breadcrumb/registry.part +18 -0
- package/assets/templates/views/category/index.tsx +76 -7
- package/assets/templates/views/category/props.ts +7 -0
- package/assets/templates/views/category/registry.tsx +19 -0
- package/assets/templates/views/category/schema.ts +13 -0
- package/assets/templates/views/home/assembly/assembly.json +9 -0
- package/assets/templates/views/home/assembly/index.skeleton.tsx +22 -4
- package/assets/templates/views/home/assembly/slots/hero/StorefrontHero1/element.part +1 -1
- package/assets/templates/views/home/assembly/slots/hero/StorefrontHero1/registry.part +2 -2
- package/assets/templates/views/home/data.ts +9 -0
- package/assets/templates/views/home/index.tsx +23 -5
- package/assets/templates/views/home/registry.tsx +2 -2
- package/assets/templates/views/product/data.ts +4 -2
- package/assets/templates/views/product/index.tsx +49 -5
- package/assets/templates/views/product/props.ts +7 -0
- package/assets/templates/views/product/registry.tsx +25 -0
- package/assets/templates/views/product/schema.ts +13 -0
- package/assets/templates/views/static/content.ts +4 -4
- package/assets/templates/views/static/index.tsx +1 -1
- package/bin/storefront.js +1231 -154
- package/examples/merchandising/category-led.json +37 -0
- package/examples/merchandising/collection-led.json +37 -0
- package/package.json +11 -7
- package/recipes/archetypes.json +9 -22
- package/recipes/catalog.json +40 -1
- package/recipes/features.json +25 -2
- package/recipes/recipe.schema.json +1 -1
- package/recipes/recipes.json +32 -30
- package/recipes/sections.json +10 -0
- package/recipes/validate.mjs +54 -0
- package/scripts/candidate.mjs +4 -4
- package/scripts/composers.mjs +25 -5
- package/scripts/env-file.mjs +86 -0
- package/scripts/file-classes.mjs +5 -0
- package/scripts/migrate-english-routes.mjs +276 -0
- package/scripts/pinning.mjs +74 -5
- package/scripts/plan-apply.mjs +149 -5
- package/scripts/reconcile.mjs +49 -11
- package/scripts/render-templates.mjs +97 -44
- package/scripts/section-config.mjs +253 -0
- package/scripts/starter.mjs +17 -0
- package/scripts/theme/color.mjs +185 -0
- package/scripts/theme/css-theme.mjs +233 -0
- package/scripts/theme/fonts.mjs +123 -0
- package/scripts/theme/import-export.mjs +125 -0
- package/scripts/theme/preset.mjs +93 -0
- package/scripts/theme/theme-plan.mjs +292 -0
- package/scripts/theme/tokens.mjs +137 -0
- package/scripts/view-assembly.mjs +47 -1
- package/skills/sf-feature/SKILL.md +32 -19
- package/skills/sf-setup/SKILL.md +79 -0
- package/skills/sf-theme-gen/SKILL.md +99 -36
- package/skills/sf-theme-gen/references/shadcn-theming.md +87 -0
- package/skills/{shadcn-theming/references/templates → sf-theme-gen/references}/theme-setup.md +15 -19
- package/skills/{shadcn-theming → sf-theme-gen}/references/theming-guide.md +6 -11
- package/skills/sf-view-gen/SKILL.md +63 -40
- package/skills/sf-view-gen/assets/recipes/archetypes.json +9 -22
- package/skills/sf-view-gen/assets/recipes/catalog.json +40 -1
- package/skills/sf-view-gen/assets/recipes/features.json +25 -2
- package/skills/sf-view-gen/assets/recipes/recipes.json +32 -30
- package/skills/sf-view-gen/assets/recipes/sections.json +10 -0
- package/skills/sf-view-gen/assets/templates/views/sections/products.client.tsx +100 -0
- package/skills/sf-view-gen/assets/templates/views/sections/products.tsx +19 -0
- package/skills/sf-view-gen/assets/templates/views/sections/schema.products.ts +39 -0
- package/skills/{json-render-core/SKILL.md → sf-view-gen/references/json-render-core.md} +4 -27
- package/skills/{json-render-next/SKILL.md → sf-view-gen/references/json-render-next.md} +5 -8
- package/skills/{json-render-react/SKILL.md → sf-view-gen/references/json-render-react.md} +3 -5
- package/skills/{json-render-shadcn/SKILL.md → sf-view-gen/references/json-render-shadcn.md} +3 -5
- package/skills/sf-view-gen/references/views.md +28 -37
- package/skills/sf-view-gen/scripts/validate.mjs +54 -0
- package/skills/storefront-workflow/SKILL.md +94 -31
- package/skills/{storefront-best-practices/reference/design.md → storefront-workflow/references/design-consistency.md} +13 -29
- package/CHANGELOG.md +0 -84
- package/assets/templates/views/user-panel/index.tsx +0 -106
- package/assets/templates/views/user-panel/registry.tsx +0 -54
- package/assets/templates/views/user-panel/schema.ts +0 -79
- package/commands/sf-add-view.md +0 -57
- package/commands/sf-feature.md +0 -28
- package/commands/sf-theme.md +0 -26
- package/commands/sf-view.md +0 -67
- package/skills/building-storefronts/SKILL.md +0 -178
- package/skills/building-storefronts/references/frontend-integration.md +0 -229
- package/skills/json-render-remotion/SKILL.md +0 -111
- package/skills/json-render-solid/SKILL.md +0 -204
- package/skills/nextjs-shadcn/SKILL.md +0 -367
- package/skills/nextjs-shadcn/references/architecture.md +0 -499
- package/skills/nextjs-shadcn/references/project-setup.md +0 -162
- package/skills/nextjs-shadcn/references/shadcn-platform.md +0 -261
- package/skills/nextjs-shadcn/references/sidebar.md +0 -274
- package/skills/nextjs-shadcn/references/styling.md +0 -555
- package/skills/shadcn-component-discovery/SKILL.md +0 -273
- package/skills/shadcn-component-discovery/references/registries.md +0 -226
- package/skills/shadcn-theming/SKILL.md +0 -104
- package/skills/storefront-best-practices/SKILL.md +0 -421
- package/skills/storefront-best-practices/reference/components/breadcrumbs.md +0 -123
- package/skills/storefront-best-practices/reference/components/cart-popup.md +0 -189
- package/skills/storefront-best-practices/reference/components/country-selector.md +0 -298
- package/skills/storefront-best-practices/reference/components/footer.md +0 -112
- package/skills/storefront-best-practices/reference/components/hero.md +0 -241
- package/skills/storefront-best-practices/reference/components/megamenu.md +0 -239
- package/skills/storefront-best-practices/reference/components/navbar.md +0 -397
- package/skills/storefront-best-practices/reference/components/popups.md +0 -221
- package/skills/storefront-best-practices/reference/components/product-card.md +0 -125
- package/skills/storefront-best-practices/reference/components/product-reviews.md +0 -217
- package/skills/storefront-best-practices/reference/components/product-slider.md +0 -174
- package/skills/storefront-best-practices/reference/components/search.md +0 -101
- package/skills/storefront-best-practices/reference/connecting-to-backend.md +0 -391
- package/skills/storefront-best-practices/reference/features/promotions.md +0 -307
- package/skills/storefront-best-practices/reference/features/wishlist.md +0 -230
- package/skills/storefront-best-practices/reference/layouts/account.md +0 -380
- package/skills/storefront-best-practices/reference/layouts/cart.md +0 -316
- package/skills/storefront-best-practices/reference/layouts/checkout.md +0 -486
- package/skills/storefront-best-practices/reference/layouts/home-page.md +0 -264
- package/skills/storefront-best-practices/reference/layouts/order-confirmation.md +0 -231
- package/skills/storefront-best-practices/reference/layouts/product-details.md +0 -527
- package/skills/storefront-best-practices/reference/layouts/product-listing.md +0 -520
- package/skills/storefront-best-practices/reference/layouts/static-pages.md +0 -356
- package/skills/storefront-best-practices/reference/medusa.md +0 -307
- /package/skills/{storefront-best-practices/reference → storefront-workflow/references}/mobile-responsiveness.md +0 -0
- /package/skills/{storefront-best-practices/reference → storefront-workflow/references}/seo.md +0 -0
package/scripts/reconcile.mjs
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
buildClientRegistryBarrel,
|
|
11
11
|
writeClientRegistryBarrel,
|
|
12
12
|
injectLayout,
|
|
13
|
+
migrateLayoutInjection,
|
|
13
14
|
injectThemeMode,
|
|
14
15
|
injectGlobals,
|
|
15
16
|
} from './render-templates.mjs'
|
|
@@ -80,18 +81,22 @@ async function renderDerivedTree(fromDir, toDir, subs, classifyRel, changed, ski
|
|
|
80
81
|
async function ensureLayoutAppend(targetDir, name, siteName, mode, dryRun) {
|
|
81
82
|
const layoutPath = path.join(targetDir, 'app', 'layout.tsx')
|
|
82
83
|
if (!existsSync(layoutPath)) {
|
|
83
|
-
if (dryRun) return true
|
|
84
|
+
if (dryRun) return { changed: true, conflict: null }
|
|
84
85
|
const raw = await readFile(path.join(templatesRoot, 'app', 'app', 'layout.tsx'), 'utf8')
|
|
85
86
|
const content = injectThemeMode(replacePlaceholders(raw, { name, siteName, mode }), mode)
|
|
86
87
|
await mkdir(path.dirname(layoutPath), { recursive: true })
|
|
87
88
|
await writeFile(layoutPath, content)
|
|
88
|
-
return true
|
|
89
|
+
return { changed: true, conflict: null }
|
|
89
90
|
}
|
|
90
91
|
const existing = await readFile(layoutPath, 'utf8')
|
|
91
|
-
|
|
92
|
-
|
|
92
|
+
// Migrate the recognized default-palette injection (CSS owns the theme now).
|
|
93
|
+
// A custom injection is reported as a conflict and nothing is written.
|
|
94
|
+
const migration = migrateLayoutInjection(existing)
|
|
95
|
+
if (migration.conflict) return { changed: false, conflict: migration.conflict }
|
|
96
|
+
const content = injectThemeMode(injectLayout(migration.content, name), mode)
|
|
97
|
+
if (content === existing) return { changed: false, conflict: null }
|
|
93
98
|
if (!dryRun) await writeFile(layoutPath, content)
|
|
94
|
-
return true
|
|
99
|
+
return { changed: true, conflict: null }
|
|
95
100
|
}
|
|
96
101
|
|
|
97
102
|
async function ensureGlobalsAppend(targetDir, name, siteName, dryRun) {
|
|
@@ -99,8 +104,9 @@ async function ensureGlobalsAppend(targetDir, name, siteName, dryRun) {
|
|
|
99
104
|
if (!existsSync(globalsPath)) {
|
|
100
105
|
if (dryRun) return true
|
|
101
106
|
const raw = await readFile(path.join(templatesRoot, 'app', 'app', 'globals.css'), 'utf8')
|
|
107
|
+
const content = injectGlobals(replacePlaceholders(raw, { name, siteName }))
|
|
102
108
|
await mkdir(path.dirname(globalsPath), { recursive: true })
|
|
103
|
-
await writeFile(globalsPath,
|
|
109
|
+
await writeFile(globalsPath, content)
|
|
104
110
|
return true
|
|
105
111
|
}
|
|
106
112
|
const existing = await readFile(globalsPath, 'utf8')
|
|
@@ -137,7 +143,12 @@ async function pruneEmptyDirs(dir) {
|
|
|
137
143
|
// template (unknown) are preserved, and their locations are returned so the
|
|
138
144
|
// caller can report them. A pristine template file is removed so a pure
|
|
139
145
|
// add/remove round-trip restores the tree exactly.
|
|
140
|
-
export async function removeViewFiles(
|
|
146
|
+
export async function removeViewFiles(
|
|
147
|
+
projectDir,
|
|
148
|
+
manifest,
|
|
149
|
+
viewName,
|
|
150
|
+
{ dryRun = false, legacyDerivedFiles = null } = {},
|
|
151
|
+
) {
|
|
141
152
|
const name = manifest.name
|
|
142
153
|
const viewDir = path.join(projectDir, 'lib', name, 'views', viewName)
|
|
143
154
|
if (!existsSync(viewDir)) return { removed: [], preserved: [] }
|
|
@@ -147,6 +158,11 @@ export async function removeViewFiles(projectDir, manifest, viewName, { dryRun =
|
|
|
147
158
|
mode: manifest.theme?.mode ?? DEFAULT_THEME_MODE,
|
|
148
159
|
}
|
|
149
160
|
const templateDir = viewTemplateDir(viewName)
|
|
161
|
+
// A retired view's template no longer exists, so its old derived files would
|
|
162
|
+
// otherwise be treated as unknown and preserved. An explicit bounded list
|
|
163
|
+
// marks exactly those derived files as known (and therefore removable); a
|
|
164
|
+
// user-owned or unknown file is still preserved by the class check below.
|
|
165
|
+
const legacySet = new Set(Array.isArray(legacyDerivedFiles) ? legacyDerivedFiles : [])
|
|
150
166
|
const removed = []
|
|
151
167
|
const preserved = []
|
|
152
168
|
for (const rel of await walkFiles(viewDir)) {
|
|
@@ -154,7 +170,7 @@ export async function removeViewFiles(projectDir, manifest, viewName, { dryRun =
|
|
|
154
170
|
const posixRel = rel.split(path.sep).join('/')
|
|
155
171
|
const fileClass = classifyFile(path.posix.join('views', viewName, posixRel))
|
|
156
172
|
const templatePath = templateDir ? path.join(templateDir, rel) : null
|
|
157
|
-
const known = Boolean(templatePath && existsSync(templatePath))
|
|
173
|
+
const known = Boolean((templatePath && existsSync(templatePath)) || legacySet.has(posixRel))
|
|
158
174
|
const maybeAuthored = !known || fileClass === 'user-owned'
|
|
159
175
|
if (maybeAuthored) {
|
|
160
176
|
let pristine = false
|
|
@@ -312,7 +328,8 @@ export async function reconcile(projectDir, manifest, { dryRun = false } = {}) {
|
|
|
312
328
|
return (
|
|
313
329
|
normalized.endsWith('views/index.ts') ||
|
|
314
330
|
normalized === '__STOREFRONT__/state.ts' ||
|
|
315
|
-
normalized === '__STOREFRONT__/handlers.ts'
|
|
331
|
+
normalized === '__STOREFRONT__/handlers.ts' ||
|
|
332
|
+
normalized === '__STOREFRONT__/composition.ts'
|
|
316
333
|
)
|
|
317
334
|
},
|
|
318
335
|
dryRun,
|
|
@@ -442,7 +459,11 @@ export async function reconcile(projectDir, manifest, { dryRun = false } = {}) {
|
|
|
442
459
|
changed.push(clientBarrelPath)
|
|
443
460
|
}
|
|
444
461
|
|
|
445
|
-
|
|
462
|
+
const layoutResult = await ensureLayoutAppend(projectDir, name, siteName, mode, dryRun)
|
|
463
|
+
if (layoutResult.conflict) {
|
|
464
|
+
return { ok: false, errors: [layoutResult.conflict], views: [], changed: [] }
|
|
465
|
+
}
|
|
466
|
+
if (layoutResult.changed) {
|
|
446
467
|
changed.push(path.join(projectDir, 'app', 'layout.tsx'))
|
|
447
468
|
}
|
|
448
469
|
if (await ensureGlobalsAppend(projectDir, name, siteName, dryRun)) {
|
|
@@ -454,6 +475,23 @@ export async function reconcile(projectDir, manifest, { dryRun = false } = {}) {
|
|
|
454
475
|
}
|
|
455
476
|
}
|
|
456
477
|
|
|
478
|
+
// The managed root guidance file is derived: rendered from the package
|
|
479
|
+
// template with the project placeholders, refreshed by sync, and reported as
|
|
480
|
+
// drift by doctor's dry-run. It is never seeded-and-frozen (not user-owned),
|
|
481
|
+
// so the rules a store enforces always match the installed CLI generation.
|
|
482
|
+
if (classifyFile('AGENTS.md') === 'derived') {
|
|
483
|
+
const agentsTemplate = path.join(templatesRoot, 'AGENTS.md')
|
|
484
|
+
if (existsSync(agentsTemplate)) {
|
|
485
|
+
const agentsPath = path.join(projectDir, 'AGENTS.md')
|
|
486
|
+
const nextAgents = replacePlaceholders(await readFile(agentsTemplate, 'utf8'), subs)
|
|
487
|
+
const prevAgents = existsSync(agentsPath) ? await readFile(agentsPath, 'utf8') : null
|
|
488
|
+
if (prevAgents !== nextAgents) {
|
|
489
|
+
if (!dryRun) await writeFile(agentsPath, nextAgents)
|
|
490
|
+
changed.push(agentsPath)
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
457
495
|
// Composer files are generated from the installed features, not templated.
|
|
458
496
|
// A feature contributes to a composer only when the registry declares it owns
|
|
459
497
|
// state/handlers for it; a pure editorial feature (no state, no handlers)
|
|
@@ -463,7 +501,7 @@ export async function reconcile(projectDir, manifest, { dryRun = false } = {}) {
|
|
|
463
501
|
for (const [composerPath, content] of [
|
|
464
502
|
[path.join(projectDir, 'lib', name, 'state.ts'), buildStateComposer(stateFeatures)],
|
|
465
503
|
[path.join(projectDir, 'lib', name, 'handlers.ts'), buildHandlersComposer(handlerFeatures)],
|
|
466
|
-
[path.join(projectDir, 'lib', name, 'composition.ts'), buildCompositionComposer(manifest.features)],
|
|
504
|
+
[path.join(projectDir, 'lib', name, 'composition.ts'), buildCompositionComposer(manifest.features, manifest.views)],
|
|
467
505
|
]) {
|
|
468
506
|
const prev = existsSync(composerPath) ? await readFile(composerPath, 'utf8') : null
|
|
469
507
|
if (prev === content) continue
|
|
@@ -17,6 +17,8 @@ import {
|
|
|
17
17
|
resolveViewGenerationSelection,
|
|
18
18
|
} from './view-selection.mjs'
|
|
19
19
|
import { getFeature } from '../recipes/validate.mjs'
|
|
20
|
+
import { fillMissingTokens } from './theme/css-theme.mjs'
|
|
21
|
+
import { REQUIRED_CSS_TOKENS, DEFAULT_GLOBALS_TOKENS } from './theme/tokens.mjs'
|
|
20
22
|
|
|
21
23
|
const here = path.dirname(fileURLToPath(import.meta.url))
|
|
22
24
|
const pkgRoot = path.resolve(here, '..')
|
|
@@ -84,8 +86,8 @@ export async function renderTree(fromDir, toDir, subs, { skip } = {}) {
|
|
|
84
86
|
}
|
|
85
87
|
|
|
86
88
|
// A generated export binding must be a valid JS identifier. View and feature
|
|
87
|
-
// names are kebab-case (`
|
|
88
|
-
// (`
|
|
89
|
+
// names are kebab-case (`static-pages`), so their binding is the camelCase form
|
|
90
|
+
// (`staticPages`). A name that cannot yield a valid binding is rejected before
|
|
89
91
|
// any derived file is written.
|
|
90
92
|
export function isValidIdentifier(value) {
|
|
91
93
|
return typeof value === 'string' && /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(value)
|
|
@@ -181,51 +183,41 @@ const THEME_MODE_MARKER = "from 'next-themes'"
|
|
|
181
183
|
export const THEME_MODES = ['light', 'dark', 'system']
|
|
182
184
|
export const DEFAULT_THEME_MODE = 'system'
|
|
183
185
|
const GLOBALS_IMPORT = '@import "@thorprovider/blocks/styles.css";'
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
//
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
//
|
|
190
|
-
|
|
186
|
+
// Marks the block color bridge in the `@theme inline` block. The success/warning
|
|
187
|
+
// *values* are filled as ordinary declarations (DEFAULT_GLOBALS_TOKENS) so the
|
|
188
|
+
// fill step and the bridge stay in sync.
|
|
189
|
+
const GLOBALS_THEME_MARKER = '--color-success:'
|
|
190
|
+
|
|
191
|
+
// The `@theme inline` bridge the scaffold guarantees on top of whatever the
|
|
192
|
+
// shadcn preset produced, so Tailwind utilities (`bg-success`, `text-warning`, …)
|
|
193
|
+
// resolve against the block tokens. Appended once; never a re-statement of the
|
|
194
|
+
// preset's own tokens.
|
|
195
|
+
const BLOCK_THEME_APPEND = `@theme inline {
|
|
191
196
|
--color-success: var(--success);
|
|
192
197
|
--color-success-foreground: var(--success-foreground);
|
|
193
198
|
--color-warning: var(--warning);
|
|
194
199
|
--color-warning-foreground: var(--warning-foreground);
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
:root {
|
|
198
|
-
--success: oklch(0.47 0.14 155);
|
|
199
|
-
--success-foreground: oklch(0.98 0.01 155);
|
|
200
|
-
--warning: oklch(0.55 0.18 75);
|
|
201
|
-
--warning-foreground: oklch(0.98 0.02 75);
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
.dark {
|
|
205
|
-
--success: oklch(0.6 0.13 155);
|
|
206
|
-
--success-foreground: oklch(0.1 0.02 155);
|
|
207
|
-
--warning: oklch(0.7 0.15 75);
|
|
208
|
-
--warning-foreground: oklch(0.1 0.02 75);
|
|
209
|
-
}
|
|
210
|
-
`
|
|
200
|
+
}`
|
|
211
201
|
|
|
212
202
|
/**
|
|
213
203
|
* Idempotently inject the storefront provider wiring into an existing
|
|
214
|
-
* app/layout.tsx (the create-next-app default). Adds the
|
|
215
|
-
*
|
|
216
|
-
*
|
|
217
|
-
*
|
|
204
|
+
* app/layout.tsx (the create-next-app default). Adds the provider and registry
|
|
205
|
+
* imports after the last import line, then wraps {children} in the provider.
|
|
206
|
+
*
|
|
207
|
+
* The provider is given NO `themeCss`: a generated single-tenant storefront owns
|
|
208
|
+
* its theme in app/globals.css, so nothing is injected late in the document to
|
|
209
|
+
* override it. A host that needs a runtime palette passes `themeCss` explicitly
|
|
210
|
+
* (see StorefrontProvider). Returns the content unchanged when the provider
|
|
211
|
+
* marker is already present (so a re-run never duplicates the injection and user
|
|
212
|
+
* edits survive).
|
|
218
213
|
*/
|
|
219
214
|
export function injectLayout(content, name) {
|
|
220
215
|
if (content.includes(LAYOUT_MARKER) || content.includes(LEGACY_LAYOUT_MARKER)) return content
|
|
221
216
|
|
|
222
217
|
const importLines = [
|
|
223
218
|
`import { StorefrontProvider } from '${LAYOUT_MARKER}'`,
|
|
224
|
-
`import { themeVarsToCss, themeTokensToVars } from '@thorprovider/runtime/render-core/theme'`,
|
|
225
|
-
`import { DEFAULT_THEME_TOKENS } from '@thorprovider/runtime/contracts'`,
|
|
226
219
|
`import { registry } from '@/lib/${name}/registry'`,
|
|
227
220
|
]
|
|
228
|
-
const constLine = 'const themeCss = themeVarsToCss(themeTokensToVars(DEFAULT_THEME_TOKENS))'
|
|
229
221
|
|
|
230
222
|
const lines = content.split('\n')
|
|
231
223
|
let lastImport = -1
|
|
@@ -234,20 +226,62 @@ export function injectLayout(content, name) {
|
|
|
234
226
|
}
|
|
235
227
|
let out
|
|
236
228
|
if (lastImport >= 0) {
|
|
237
|
-
lines.splice(lastImport + 1, 0, '', ...importLines
|
|
229
|
+
lines.splice(lastImport + 1, 0, '', ...importLines)
|
|
238
230
|
out = lines.join('\n')
|
|
239
231
|
} else {
|
|
240
|
-
out = [...importLines, '',
|
|
232
|
+
out = [...importLines, '', content].join('\n')
|
|
241
233
|
}
|
|
242
234
|
|
|
243
235
|
if (!/\{children\}/.test(out)) return out
|
|
244
236
|
out = out.replace(
|
|
245
237
|
/\{children\}/,
|
|
246
|
-
'<StorefrontProvider
|
|
238
|
+
'<StorefrontProvider registry={registry}>{children}</StorefrontProvider>',
|
|
247
239
|
)
|
|
248
240
|
return out
|
|
249
241
|
}
|
|
250
242
|
|
|
243
|
+
// The exact default theme expression the previous scaffold injected. Migration
|
|
244
|
+
// removes it so the stylesheet becomes the single visual source.
|
|
245
|
+
const LEGACY_THEME_EXPR = 'themeVarsToCss(themeTokensToVars(DEFAULT_THEME_TOKENS))'
|
|
246
|
+
const LEGACY_THEME_IMPORT = `import { themeVarsToCss, themeTokensToVars } from '@thorprovider/runtime/render-core/theme'`
|
|
247
|
+
const LEGACY_TOKENS_IMPORT = `import { DEFAULT_THEME_TOKENS } from '@thorprovider/runtime/contracts'`
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Migrate the recognized default-palette injection out of an existing
|
|
251
|
+
* app/layout.tsx so app/globals.css owns the theme. Removes only the known
|
|
252
|
+
* runtime imports, the known `const themeCss = …DEFAULT_THEME_TOKENS…` line and
|
|
253
|
+
* the `themeCss={themeCss}` attribute.
|
|
254
|
+
*
|
|
255
|
+
* Returns `{ content, changed, migrated, conflict }`. When the file injects a
|
|
256
|
+
* *custom* themeCss expression (an unrecognized palette), it reports `conflict`
|
|
257
|
+
* and returns the content unchanged — the CLI never guesses away a developer's
|
|
258
|
+
* own injection. A file already CSS-owned (no `themeCss`) is returned unchanged.
|
|
259
|
+
*/
|
|
260
|
+
export function migrateLayoutInjection(content) {
|
|
261
|
+
const result = { content, changed: false, migrated: false, conflict: null }
|
|
262
|
+
if (!content.includes(LAYOUT_MARKER)) return result
|
|
263
|
+
if (!/themeCss/.test(content)) return result
|
|
264
|
+
|
|
265
|
+
const hasDefaultExpr = content.includes(`const themeCss = ${LEGACY_THEME_EXPR}`)
|
|
266
|
+
const hasThemeImport = content.includes(LEGACY_THEME_IMPORT)
|
|
267
|
+
if (!hasDefaultExpr || !hasThemeImport) {
|
|
268
|
+
result.conflict =
|
|
269
|
+
'app/layout.tsx injects a custom `themeCss` palette that the CLI does not recognize. ' +
|
|
270
|
+
'Remove the `themeCss` prop (theme now lives in app/globals.css) or migrate it by hand, then re-run.'
|
|
271
|
+
return result
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const next = content
|
|
275
|
+
.replace(`${LEGACY_THEME_IMPORT}\n`, '')
|
|
276
|
+
.replace(`${LEGACY_TOKENS_IMPORT}\n`, '')
|
|
277
|
+
.replace(/^\s*const themeCss = themeVarsToCss\(themeTokensToVars\(DEFAULT_THEME_TOKENS\)\)\n/m, '')
|
|
278
|
+
.replace(/themeCss=\{themeCss\}\s*/g, '')
|
|
279
|
+
result.content = next
|
|
280
|
+
result.changed = next !== content
|
|
281
|
+
result.migrated = true
|
|
282
|
+
return result
|
|
283
|
+
}
|
|
284
|
+
|
|
251
285
|
/**
|
|
252
286
|
* Idempotently wire the next-themes ThemeProvider into app/layout.tsx so the
|
|
253
287
|
* scaffolded site can render light/dark/system. `attribute="class"` matches the
|
|
@@ -305,11 +339,14 @@ export function injectThemeMode(content, mode = DEFAULT_THEME_MODE) {
|
|
|
305
339
|
}
|
|
306
340
|
|
|
307
341
|
/**
|
|
308
|
-
* Idempotently
|
|
309
|
-
*
|
|
310
|
-
*
|
|
311
|
-
*
|
|
312
|
-
*
|
|
342
|
+
* Idempotently complete an existing app/globals.css (produced by create-next-app
|
|
343
|
+
* + shadcn init): append the blocks stylesheet import, append the block color
|
|
344
|
+
* bridge (`@theme inline`), and fill every missing required token declaration in
|
|
345
|
+
* `:root` / `.dark` from DEFAULT_GLOBALS_TOKENS. Existing declarations are never
|
|
346
|
+
* replaced — a developer's customized value always wins — so the generated
|
|
347
|
+
* stylesheet is the complete, single source of the theme (the runtime palette
|
|
348
|
+
* injection is no longer emitted). Returns the content unchanged when nothing is
|
|
349
|
+
* missing.
|
|
313
350
|
*/
|
|
314
351
|
export function injectGlobals(content) {
|
|
315
352
|
const lines = content.split('\n')
|
|
@@ -327,8 +364,11 @@ export function injectGlobals(content) {
|
|
|
327
364
|
out = `${GLOBALS_IMPORT}\n${content}`
|
|
328
365
|
}
|
|
329
366
|
|
|
330
|
-
if (out.includes(
|
|
331
|
-
|
|
367
|
+
if (!out.includes(GLOBALS_THEME_MARKER)) {
|
|
368
|
+
out = `${out.replace(/\s*$/, '')}\n\n${BLOCK_THEME_APPEND}\n`
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
return fillMissingTokens(out, REQUIRED_CSS_TOKENS, DEFAULT_GLOBALS_TOKENS)
|
|
332
372
|
}
|
|
333
373
|
|
|
334
374
|
/**
|
|
@@ -417,10 +457,21 @@ export async function renderScaffold({
|
|
|
417
457
|
const handlerFeatures = featureNames.filter((n) => (getFeature(n)?.handlers?.length ?? 0) > 0)
|
|
418
458
|
await writeFile(path.join(libDir, 'state.ts'), buildStateComposer(stateFeatures))
|
|
419
459
|
await writeFile(path.join(libDir, 'handlers.ts'), buildHandlersComposer(handlerFeatures))
|
|
420
|
-
await writeFile(path.join(libDir, 'composition.ts'), buildCompositionComposer(featureEntries))
|
|
460
|
+
await writeFile(path.join(libDir, 'composition.ts'), buildCompositionComposer(featureEntries, viewEntries))
|
|
421
461
|
|
|
422
462
|
await ensureLayoutAppend(targetDir, name, siteName, mode)
|
|
423
463
|
await ensureGlobalsAppend(targetDir, name, siteName)
|
|
464
|
+
await renderRootAgentsMd(targetDir, { name, siteName, mode })
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// The managed root guidance file ships with the scaffold: rendered from the
|
|
468
|
+
// package template with the project placeholders, exactly as reconcile renders
|
|
469
|
+
// it, so a fresh scaffold and a later sync agree byte-for-byte.
|
|
470
|
+
async function renderRootAgentsMd(targetDir, subs) {
|
|
471
|
+
const templatePath = path.join(templatesRoot, 'AGENTS.md')
|
|
472
|
+
if (!existsSync(templatePath)) return
|
|
473
|
+
const raw = await readFile(templatePath, 'utf8')
|
|
474
|
+
await writeFile(path.join(targetDir, 'AGENTS.md'), replacePlaceholders(raw, subs))
|
|
424
475
|
}
|
|
425
476
|
|
|
426
477
|
async function ensureLayoutAppend(targetDir, name, siteName, mode = DEFAULT_THEME_MODE) {
|
|
@@ -441,7 +492,9 @@ async function ensureGlobalsAppend(targetDir, name, siteName) {
|
|
|
441
492
|
const templatePath = path.join(templatesRoot, 'app', 'app', 'globals.css')
|
|
442
493
|
if (!existsSync(globalsPath)) {
|
|
443
494
|
const raw = await readFile(templatePath, 'utf8')
|
|
444
|
-
|
|
495
|
+
// Run the completeness fill on the fallback template too, so a fresh project
|
|
496
|
+
// and a shadcn-generated one end with the same required token set.
|
|
497
|
+
await writeFile(globalsPath, injectGlobals(replacePlaceholders(raw, { name, siteName })))
|
|
445
498
|
return
|
|
446
499
|
}
|
|
447
500
|
const existing = await readFile(globalsPath, 'utf8')
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
|
|
4
|
+
// User-owned source configuration audit.
|
|
5
|
+
//
|
|
6
|
+
// Data-bound section instances (for example the `products` type) resolve their
|
|
7
|
+
// content from a canonical query the user declares in a user-owned `data.ts`
|
|
8
|
+
// export named `sectionSources`. The manifest records only the structural
|
|
9
|
+
// instance (`{ id, type, variant }`); the query lives in the user file. `sync`
|
|
10
|
+
// never patches user files, so a freshly added instance is intentionally
|
|
11
|
+
// unconfigured until the user supplies its entry. `doctor` reports those gaps
|
|
12
|
+
// with the file and instance id so the repair is actionable.
|
|
13
|
+
//
|
|
14
|
+
// The audit is a conservative static read of the object literal: it extracts
|
|
15
|
+
// the top-level keys of `sectionSources` and whether each value is an object.
|
|
16
|
+
// It never executes the user file and never rewrites it. Deep validity (the
|
|
17
|
+
// canonical query fields) is enforced at build/runtime by the runtime
|
|
18
|
+
// `sectionSourceSchema`, which surfaces an `unconfigured` state for the
|
|
19
|
+
// instance rather than failing the page.
|
|
20
|
+
|
|
21
|
+
/** Locate the `sectionSources` declaration and return the index of its name. */
|
|
22
|
+
function findSectionSourcesIndex(source) {
|
|
23
|
+
const match = /\bsectionSources\b/.exec(source)
|
|
24
|
+
return match ? match.index : -1
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Match the brace that opens at `open` and return the index of its closer,
|
|
29
|
+
* ignoring braces inside string literals and comments. Returns -1 when
|
|
30
|
+
* unbalanced.
|
|
31
|
+
*/
|
|
32
|
+
function matchBrace(source, open) {
|
|
33
|
+
let depth = 0
|
|
34
|
+
let i = open
|
|
35
|
+
while (i < source.length) {
|
|
36
|
+
const ch = source[i]
|
|
37
|
+
if (ch === '/' && source[i + 1] === '/') {
|
|
38
|
+
i = source.indexOf('\n', i)
|
|
39
|
+
if (i < 0) return -1
|
|
40
|
+
continue
|
|
41
|
+
}
|
|
42
|
+
if (ch === '/' && source[i + 1] === '*') {
|
|
43
|
+
const end = source.indexOf('*/', i + 2)
|
|
44
|
+
if (end < 0) return -1
|
|
45
|
+
i = end + 2
|
|
46
|
+
continue
|
|
47
|
+
}
|
|
48
|
+
if (ch === '"' || ch === "'" || ch === '`') {
|
|
49
|
+
i = skipString(source, i)
|
|
50
|
+
if (i < 0) return -1
|
|
51
|
+
continue
|
|
52
|
+
}
|
|
53
|
+
if (ch === '{') depth++
|
|
54
|
+
else if (ch === '}') {
|
|
55
|
+
depth--
|
|
56
|
+
if (depth === 0) return i
|
|
57
|
+
}
|
|
58
|
+
i++
|
|
59
|
+
}
|
|
60
|
+
return -1
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Advance past a string literal starting at `start`; returns the index after it. */
|
|
64
|
+
function skipString(source, start) {
|
|
65
|
+
const quote = source[start]
|
|
66
|
+
let i = start + 1
|
|
67
|
+
while (i < source.length) {
|
|
68
|
+
if (source[i] === '\\') {
|
|
69
|
+
i += 2
|
|
70
|
+
continue
|
|
71
|
+
}
|
|
72
|
+
if (source[i] === quote) return i + 1
|
|
73
|
+
i++
|
|
74
|
+
}
|
|
75
|
+
return -1
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Advance past a value starting at `start`, returning the index at depth 0 after it. */
|
|
79
|
+
function skipValue(source, start) {
|
|
80
|
+
let depth = 0
|
|
81
|
+
let i = start
|
|
82
|
+
while (i < source.length) {
|
|
83
|
+
const ch = source[i]
|
|
84
|
+
if (ch === '/' && source[i + 1] === '/') {
|
|
85
|
+
const nl = source.indexOf('\n', i)
|
|
86
|
+
if (nl < 0) return source.length
|
|
87
|
+
i = nl + 1
|
|
88
|
+
continue
|
|
89
|
+
}
|
|
90
|
+
if (ch === '/' && source[i + 1] === '*') {
|
|
91
|
+
const end = source.indexOf('*/', i + 2)
|
|
92
|
+
if (end < 0) return source.length
|
|
93
|
+
i = end + 2
|
|
94
|
+
continue
|
|
95
|
+
}
|
|
96
|
+
if (ch === '"' || ch === "'" || ch === '`') {
|
|
97
|
+
const next = skipString(source, i)
|
|
98
|
+
if (next < 0) return source.length
|
|
99
|
+
i = next
|
|
100
|
+
continue
|
|
101
|
+
}
|
|
102
|
+
if (ch === '{' || ch === '[' || ch === '(') depth++
|
|
103
|
+
else if (ch === '}' || ch === ']' || ch === ')') {
|
|
104
|
+
if (depth === 0) return i
|
|
105
|
+
depth--
|
|
106
|
+
} else if (ch === ',' && depth === 0) return i
|
|
107
|
+
i++
|
|
108
|
+
}
|
|
109
|
+
return source.length
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Read an object key at `start` (quoted string or bare identifier). */
|
|
113
|
+
function readKey(source, start) {
|
|
114
|
+
const ch = source[start]
|
|
115
|
+
if (ch === '"' || ch === "'" || ch === '`') {
|
|
116
|
+
const end = skipString(source, start)
|
|
117
|
+
if (end < 0) return null
|
|
118
|
+
return { key: source.slice(start + 1, end - 1), next: end }
|
|
119
|
+
}
|
|
120
|
+
let i = start
|
|
121
|
+
while (i < source.length && /[A-Za-z0-9_$]/.test(source[i])) i++
|
|
122
|
+
if (i === start) return null
|
|
123
|
+
return { key: source.slice(start, i), next: i }
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function skipWhitespaceAndComments(source, start) {
|
|
127
|
+
let i = start
|
|
128
|
+
while (i < source.length) {
|
|
129
|
+
const ch = source[i]
|
|
130
|
+
if (/\s/.test(ch)) {
|
|
131
|
+
i++
|
|
132
|
+
continue
|
|
133
|
+
}
|
|
134
|
+
if (ch === '/' && source[i + 1] === '/') {
|
|
135
|
+
const nl = source.indexOf('\n', i)
|
|
136
|
+
if (nl < 0) return source.length
|
|
137
|
+
i = nl + 1
|
|
138
|
+
continue
|
|
139
|
+
}
|
|
140
|
+
if (ch === '/' && source[i + 1] === '*') {
|
|
141
|
+
const end = source.indexOf('*/', i + 2)
|
|
142
|
+
if (end < 0) return source.length
|
|
143
|
+
i = end + 2
|
|
144
|
+
continue
|
|
145
|
+
}
|
|
146
|
+
break
|
|
147
|
+
}
|
|
148
|
+
return i
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Extract the top-level entries of the `sectionSources` object literal in a
|
|
153
|
+
* `data.ts` source. Returns a Map of key -> `{ objectValue }` (whether the
|
|
154
|
+
* declared value is an object literal). An absent or unparsable declaration
|
|
155
|
+
* yields an empty Map.
|
|
156
|
+
*/
|
|
157
|
+
export function extractSectionSourceEntries(source) {
|
|
158
|
+
const entries = new Map()
|
|
159
|
+
if (typeof source !== 'string') return entries
|
|
160
|
+
const declIndex = findSectionSourcesIndex(source)
|
|
161
|
+
if (declIndex < 0) return entries
|
|
162
|
+
const bodyStart = source.indexOf('{', declIndex)
|
|
163
|
+
if (bodyStart < 0) return entries
|
|
164
|
+
const bodyEnd = matchBrace(source, bodyStart)
|
|
165
|
+
if (bodyEnd < 0) return entries
|
|
166
|
+
const body = source.slice(bodyStart + 1, bodyEnd)
|
|
167
|
+
let i = 0
|
|
168
|
+
while (i < body.length) {
|
|
169
|
+
i = skipWhitespaceAndComments(body, i)
|
|
170
|
+
if (i >= body.length) break
|
|
171
|
+
const key = readKey(body, i)
|
|
172
|
+
if (!key) break
|
|
173
|
+
const colon = skipWhitespaceAndComments(body, key.next)
|
|
174
|
+
if (body[colon] !== ':') break
|
|
175
|
+
const valueStart = skipWhitespaceAndComments(body, colon + 1)
|
|
176
|
+
entries.set(key.key, { objectValue: body[valueStart] === '{' })
|
|
177
|
+
i = skipValue(body, valueStart)
|
|
178
|
+
if (body[i] === ',') i++
|
|
179
|
+
}
|
|
180
|
+
return entries
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* The data-bound section instances declared across a manifest's views, in
|
|
185
|
+
* order. `isDataBound` is the predicate that identifies a data-bound type
|
|
186
|
+
* (typically `sectionIsDataBound` from `recipes/validate.mjs`).
|
|
187
|
+
*/
|
|
188
|
+
export function collectDataBoundInstances(manifest, isDataBound) {
|
|
189
|
+
const instances = []
|
|
190
|
+
const views = Array.isArray(manifest?.views) ? manifest.views : []
|
|
191
|
+
for (const view of views) {
|
|
192
|
+
const sections = Array.isArray(view?.sections) ? view.sections : []
|
|
193
|
+
for (const section of sections) {
|
|
194
|
+
if (!section || typeof section.type !== 'string') continue
|
|
195
|
+
if (!isDataBound(section.type)) continue
|
|
196
|
+
instances.push({
|
|
197
|
+
view: view.name,
|
|
198
|
+
id: section.id,
|
|
199
|
+
type: section.type,
|
|
200
|
+
variant: typeof section.variant === 'string' ? section.variant : null,
|
|
201
|
+
})
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return instances
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Audit a project's user-owned source configuration against its data-bound
|
|
209
|
+
* section instances. Returns `{ diagnostics }` where each diagnostic carries
|
|
210
|
+
* the owning view, the instance id/type, the user-owned file, and a repair
|
|
211
|
+
* message. Missing declarations and non-object declarations are both reported;
|
|
212
|
+
* deep query validity is enforced at build/runtime, not here.
|
|
213
|
+
*/
|
|
214
|
+
export function auditSectionConfig({ dir, manifest, isDataBound }) {
|
|
215
|
+
const diagnostics = []
|
|
216
|
+
const name = typeof manifest?.name === 'string' && manifest.name.length > 0 ? manifest.name : 'storefront'
|
|
217
|
+
const instances = collectDataBoundInstances(manifest, isDataBound)
|
|
218
|
+
const byView = new Map()
|
|
219
|
+
for (const instance of instances) {
|
|
220
|
+
if (!byView.has(instance.view)) byView.set(instance.view, [])
|
|
221
|
+
byView.get(instance.view).push(instance)
|
|
222
|
+
}
|
|
223
|
+
for (const [view, viewInstances] of byView) {
|
|
224
|
+
const relPath = `lib/${name}/views/${view}/data.ts`
|
|
225
|
+
const absPath = join(dir, relPath)
|
|
226
|
+
const entries = existsSync(absPath)
|
|
227
|
+
? extractSectionSourceEntries(readFileSync(absPath, 'utf8'))
|
|
228
|
+
: null
|
|
229
|
+
for (const instance of viewInstances) {
|
|
230
|
+
const entry = entries?.get(instance.id)
|
|
231
|
+
if (!entry) {
|
|
232
|
+
diagnostics.push({
|
|
233
|
+
code: 'E_SECTION_CONFIG_MISSING',
|
|
234
|
+
view,
|
|
235
|
+
id: instance.id,
|
|
236
|
+
type: instance.type,
|
|
237
|
+
path: relPath,
|
|
238
|
+
message: `section "${instance.id}" (${instance.type}) is not configured; add a sectionSources["${instance.id}"] entry in ${relPath}`,
|
|
239
|
+
})
|
|
240
|
+
} else if (!entry.objectValue) {
|
|
241
|
+
diagnostics.push({
|
|
242
|
+
code: 'E_SECTION_CONFIG_INVALID',
|
|
243
|
+
view,
|
|
244
|
+
id: instance.id,
|
|
245
|
+
type: instance.type,
|
|
246
|
+
path: relPath,
|
|
247
|
+
message: `sectionSources["${instance.id}"] in ${relPath} must be an object literal (kind/query/maxItems)`,
|
|
248
|
+
})
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return { diagnostics }
|
|
253
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Initial composition for `storefront create`.
|
|
3
|
+
//
|
|
4
|
+
// Without `--starter`, create seeds the minimal baseline (home + cart, no
|
|
5
|
+
// features). With `--starter`, it seeds the full curated set: every curated
|
|
6
|
+
// feature and every curated view — including the views a feature owns, so each
|
|
7
|
+
// installed feature satisfies the composition contract. The set is derived from
|
|
8
|
+
// the registry (`recipes/validate.mjs`), never a hand-maintained list, so held
|
|
9
|
+
// units are excluded by construction.
|
|
10
|
+
|
|
11
|
+
import { curatedViewNames, starterComposition } from '../recipes/validate.mjs'
|
|
12
|
+
|
|
13
|
+
export function scaffoldComposition({ starter = false } = {}) {
|
|
14
|
+
if (!starter) return { views: ['home', 'cart'], features: [] }
|
|
15
|
+
const { features } = starterComposition()
|
|
16
|
+
return { views: curatedViewNames(), features }
|
|
17
|
+
}
|