@thorprovider/storefront 0.3.1 → 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 +321 -152
- 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 +54 -2
- 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 +17 -7
- package/assets/templates/app/app/robots.ts +19 -0
- package/assets/templates/app/app/sitemap.ts +71 -0
- package/assets/templates/app/lib/__STOREFRONT__/actions.ts +612 -0
- package/assets/templates/app/lib/__STOREFRONT__/cart-state.ts +66 -0
- package/assets/templates/app/lib/__STOREFRONT__/catalog.ts +50 -57
- package/assets/templates/app/lib/__STOREFRONT__/commerce.ts +679 -0
- package/assets/templates/app/lib/__STOREFRONT__/composition.ts +26 -0
- package/assets/templates/app/lib/__STOREFRONT__/handlers.core.ts +197 -0
- package/assets/templates/app/lib/__STOREFRONT__/handlers.ts +5 -36
- package/assets/templates/app/lib/__STOREFRONT__/icon-map.ts +27 -0
- package/assets/templates/app/lib/__STOREFRONT__/mappers.ts +13 -11
- package/assets/templates/app/lib/__STOREFRONT__/registry.tsx +264 -51
- package/assets/templates/app/lib/__STOREFRONT__/runtime.ts +6 -3
- package/assets/templates/app/lib/__STOREFRONT__/schema.ts +136 -0
- 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__/session.core.ts +470 -0
- package/assets/templates/app/lib/__STOREFRONT__/session.ts +169 -0
- package/assets/templates/app/lib/__STOREFRONT__/shell/content.ts +120 -0
- package/assets/templates/app/lib/__STOREFRONT__/shell/props.ts +38 -0
- package/assets/templates/app/lib/__STOREFRONT__/spec/index.ts +129 -10
- package/assets/templates/app/lib/__STOREFRONT__/spec/page-root.ts +5 -1
- package/assets/templates/app/lib/__STOREFRONT__/spec/seo.ts +191 -0
- package/assets/templates/app/lib/__STOREFRONT__/state.core.ts +257 -0
- package/assets/templates/app/lib/__STOREFRONT__/state.ts +7 -31
- package/assets/templates/app/lib/__STOREFRONT__/store-context.ts +161 -0
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/cta.client.tsx +25 -0
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/cta.tsx +16 -0
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/faq.client.tsx +24 -0
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/faq.tsx +5 -18
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/index.ts +18 -0
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/logoCloud.tsx +26 -14
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/pricing.tsx +36 -18
- 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.cta.ts +19 -0
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/schema.faq.ts +18 -0
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/schema.products.ts +39 -0
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/statistics.tsx +35 -14
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/testimonials.tsx +31 -13
- package/assets/templates/app/lib/__STOREFRONT__/views/types.ts +5 -3
- package/assets/templates/features/checkout/handlers.ts +159 -0
- package/assets/templates/features/checkout/state.ts +61 -0
- package/assets/templates/features/customer-auth/handlers.ts +59 -0
- package/assets/templates/features/customer-auth/state.ts +45 -0
- package/assets/templates/features/recommendations/state.ts +11 -0
- package/assets/templates/views/account/content.ts +35 -0
- package/assets/templates/views/account/index.tsx +114 -0
- package/assets/templates/views/account/registry.tsx +439 -0
- package/assets/templates/views/account/schema.ts +53 -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 +29 -0
- package/assets/templates/views/category/assembly/index.skeleton.tsx +251 -0
- package/assets/templates/views/category/assembly/registry.skeleton.tsx +34 -0
- 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/assembly/slots/filter/CategoryFilter2/catalog.part +5 -0
- package/assets/templates/views/category/assembly/slots/filter/CategoryFilter2/element.part +34 -0
- package/assets/templates/views/category/assembly/slots/filter/CategoryFilter2/registry.part +71 -0
- package/assets/templates/views/category/content.ts +30 -0
- package/assets/templates/views/category/data.ts +12 -0
- package/assets/templates/views/category/index.tsx +299 -0
- package/assets/templates/views/category/props.ts +23 -0
- package/assets/templates/views/category/registry.tsx +122 -0
- package/assets/templates/views/category/schema.ts +91 -0
- package/assets/templates/views/checkout/content.ts +105 -0
- package/assets/templates/views/checkout/data.ts +23 -0
- package/assets/templates/views/checkout/index.tsx +111 -0
- package/assets/templates/views/checkout/props.ts +49 -0
- package/assets/templates/views/checkout/registry.tsx +217 -0
- package/assets/templates/views/checkout/schema.ts +229 -0
- package/assets/templates/views/home/assembly/assembly.json +33 -0
- package/assets/templates/views/home/assembly/index.skeleton.tsx +90 -0
- package/assets/templates/views/home/assembly/registry.skeleton.tsx +25 -0
- package/assets/templates/views/home/assembly/slots/benefits/Feature1/binding.json +5 -0
- package/assets/templates/views/home/assembly/slots/benefits/Feature1/catalog.part +4 -0
- package/assets/templates/views/home/assembly/slots/benefits/Feature1/element.part +4 -0
- package/assets/templates/views/home/assembly/slots/benefits/Feature1/registry.part +11 -0
- package/assets/templates/views/home/assembly/slots/benefits/Feature2/binding.json +5 -0
- package/assets/templates/views/home/assembly/slots/benefits/Feature2/catalog.part +4 -0
- package/assets/templates/views/home/assembly/slots/benefits/Feature2/element.part +4 -0
- package/assets/templates/views/home/assembly/slots/benefits/Feature2/registry.part +10 -0
- package/assets/templates/views/home/assembly/slots/categories/ProductCategory2/binding.json +5 -0
- package/assets/templates/views/home/assembly/slots/categories/ProductCategory2/catalog.part +4 -0
- package/assets/templates/views/home/assembly/slots/categories/ProductCategory2/element.part +5 -0
- package/assets/templates/views/home/assembly/slots/categories/ProductCategory2/registry.part +16 -0
- package/assets/templates/views/home/assembly/slots/hero/StorefrontHero1/binding.json +5 -0
- package/assets/templates/views/home/assembly/slots/hero/StorefrontHero1/catalog.part +4 -0
- package/assets/templates/views/home/assembly/slots/hero/StorefrontHero1/element.part +5 -0
- package/assets/templates/views/home/assembly/slots/hero/StorefrontHero1/registry.part +26 -0
- package/assets/templates/views/home/content.ts +70 -0
- package/assets/templates/views/home/data.ts +24 -0
- package/assets/templates/views/home/index.tsx +57 -85
- package/assets/templates/views/home/props.ts +22 -0
- package/assets/templates/views/home/registry.tsx +38 -22
- package/assets/templates/views/home/schema.ts +93 -0
- package/assets/templates/views/product/content.ts +25 -0
- package/assets/templates/views/product/data.ts +50 -0
- package/assets/templates/views/product/index.tsx +148 -0
- package/assets/templates/views/product/props.ts +27 -0
- package/assets/templates/views/product/registry.tsx +103 -0
- package/assets/templates/views/product/schema.ts +121 -0
- package/assets/templates/views/static/content.ts +99 -0
- package/assets/templates/views/static/index.tsx +60 -0
- package/assets/templates/views/static/registry.tsx +66 -0
- package/assets/templates/views/static/schema.ts +45 -0
- package/bin/storefront.js +2424 -148
- package/examples/merchandising/category-led.json +37 -0
- package/examples/merchandising/collection-led.json +37 -0
- package/package.json +15 -9
- package/recipes/archetypes.json +28 -14
- package/recipes/catalog.json +400 -0
- package/recipes/catalog.mjs +128 -0
- package/recipes/catalog.schema.json +64 -0
- package/recipes/feature.schema.json +89 -0
- package/recipes/features.json +162 -0
- package/recipes/manifest.schema.json +69 -0
- package/recipes/operations.schema.json +43 -0
- package/recipes/recipe.schema.json +11 -2
- package/recipes/recipes.json +76 -19
- package/recipes/sections.json +34 -5
- package/recipes/validate.mjs +838 -32
- package/scripts/candidate.mjs +236 -0
- package/scripts/composers.mjs +160 -0
- package/scripts/env-file.mjs +86 -0
- package/scripts/file-classes.mjs +121 -0
- package/scripts/logical-controls.mjs +61 -0
- package/scripts/manifest.mjs +298 -0
- package/scripts/migrate-english-routes.mjs +276 -0
- package/scripts/operation-registry.mjs +256 -0
- package/scripts/operations.mjs +720 -0
- package/scripts/ownership-conflict.mjs +356 -0
- package/scripts/pinning.mjs +573 -0
- package/scripts/plan-apply.mjs +987 -0
- package/scripts/preflight.mjs +51 -0
- package/scripts/reconcile.mjs +549 -0
- package/scripts/render-templates.mjs +338 -68
- package/scripts/schema-validate.mjs +132 -0
- package/scripts/section-compat.mjs +300 -0
- package/scripts/section-config.mjs +253 -0
- package/scripts/section-operations.mjs +508 -0
- package/scripts/starter.mjs +17 -0
- package/scripts/sync-assets.mjs +22 -15
- 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 +337 -0
- package/scripts/view-selection.mjs +211 -0
- package/skills/sf-feature/SKILL.md +80 -0
- package/skills/sf-setup/SKILL.md +79 -0
- package/skills/sf-theme-gen/SKILL.md +100 -30
- 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 +146 -93
- package/skills/sf-view-gen/assets/recipes/archetypes.json +28 -14
- package/skills/sf-view-gen/assets/recipes/catalog.json +400 -0
- package/skills/sf-view-gen/assets/recipes/catalog.schema.json +64 -0
- package/skills/sf-view-gen/assets/recipes/features.json +162 -0
- package/skills/sf-view-gen/assets/recipes/recipes.json +76 -19
- package/skills/sf-view-gen/assets/recipes/sections.json +34 -5
- package/skills/sf-view-gen/assets/templates/views/sections/cta.client.tsx +25 -0
- package/skills/sf-view-gen/assets/templates/views/sections/cta.tsx +16 -0
- package/skills/sf-view-gen/assets/templates/views/sections/faq.client.tsx +13 -9
- package/skills/sf-view-gen/assets/templates/views/sections/faq.tsx +2 -7
- package/skills/sf-view-gen/assets/templates/views/sections/logoCloud.client.tsx +12 -8
- package/skills/sf-view-gen/assets/templates/views/sections/logoCloud.tsx +2 -7
- package/skills/sf-view-gen/assets/templates/views/sections/pricing.client.tsx +14 -10
- package/skills/sf-view-gen/assets/templates/views/sections/pricing.tsx +2 -9
- 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.cta.ts +19 -0
- package/skills/sf-view-gen/assets/templates/views/sections/schema.faq.ts +18 -0
- package/skills/sf-view-gen/assets/templates/views/sections/schema.logoCloud.ts +19 -0
- package/skills/sf-view-gen/assets/templates/views/sections/schema.pricing.ts +26 -0
- package/skills/sf-view-gen/assets/templates/views/sections/schema.products.ts +39 -0
- package/skills/sf-view-gen/assets/templates/views/sections/schema.statistics.ts +25 -0
- package/skills/sf-view-gen/assets/templates/views/sections/schema.testimonials.ts +23 -0
- package/skills/sf-view-gen/assets/templates/views/sections/statistics.client.tsx +16 -8
- package/skills/sf-view-gen/assets/templates/views/sections/statistics.tsx +2 -7
- package/skills/sf-view-gen/assets/templates/views/sections/testimonials.client.tsx +13 -9
- package/skills/sf-view-gen/assets/templates/views/sections/testimonials.tsx +2 -5
- 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 +58 -33
- package/skills/sf-view-gen/scripts/catalog.mjs +128 -0
- package/skills/sf-view-gen/scripts/schema-validate.mjs +132 -0
- package/skills/sf-view-gen/scripts/validate.mjs +838 -32
- package/skills/storefront-workflow/SKILL.md +179 -0
- package/skills/{storefront-best-practices/reference/design.md → storefront-workflow/references/design-consistency.md} +13 -29
- package/CHANGELOG.md +0 -61
- package/commands/sf-add-view.md +0 -26
- package/commands/sf-theme.md +0 -20
- package/commands/sf-view.md +0 -25
- 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/sf-view-gen/assets/templates/views/category/index.tsx +0 -86
- package/skills/sf-view-gen/assets/templates/views/category/registry.tsx +0 -48
- package/skills/sf-view-gen/assets/templates/views/checkout/index.tsx +0 -192
- package/skills/sf-view-gen/assets/templates/views/checkout/registry.tsx +0 -45
- package/skills/sf-view-gen/assets/templates/views/product/index.tsx +0 -122
- package/skills/sf-view-gen/assets/templates/views/product/registry.tsx +0 -49
- package/skills/sf-view-gen/assets/templates/views/user-panel/index.tsx +0 -168
- package/skills/sf-view-gen/assets/templates/views/user-panel/registry.tsx +0 -49
- 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
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Deterministic normalization and persisted legacy-id migration for a view's
|
|
3
|
+
// ordered `views[].sections` composition (E3 task 1.5, CU-15/CU-16).
|
|
4
|
+
//
|
|
5
|
+
// The pure entry point maps a declared section list — possibly missing, empty,
|
|
6
|
+
// or in the pre-E3 legacy `{ name, variant }` shape — to the ordered
|
|
7
|
+
// `{ id, type, variant? }` model:
|
|
8
|
+
// * a registered presentation slot is `{ id: "slot:<slot>", type: "<slot>" }`
|
|
9
|
+
// and is mandatory: the normalized list always contains every registered
|
|
10
|
+
// slot exactly once, in registry order unless the declared list reorders it;
|
|
11
|
+
// * a legacy entry migrates to `{ id: "<name>-<n>", type: "<name>",
|
|
12
|
+
// variant? }`, where `n` is the 1-based position among the view's legacy
|
|
13
|
+
// entries in declared order, appended after the slots;
|
|
14
|
+
// * anything that cannot be adapted safely (a held/disallowed legacy type, a
|
|
15
|
+
// reserved-prefixed id, a duplicate) is refused with a diagnostic and no
|
|
16
|
+
// output, so the caller writes nothing.
|
|
17
|
+
//
|
|
18
|
+
// It is a pure function over its arguments (same input -> identical output) and
|
|
19
|
+
// depends only on the Node standard library. Wiring it into the manifest
|
|
20
|
+
// load/validation and CLI paths is owned by the later migration/operation tasks
|
|
21
|
+
// (§3/§5); this module does not touch the filesystem beyond reading the registry
|
|
22
|
+
// for the optional convenience wrapper.
|
|
23
|
+
|
|
24
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
25
|
+
import { dirname, join } from 'node:path'
|
|
26
|
+
import { fileURLToPath } from 'node:url'
|
|
27
|
+
|
|
28
|
+
const here = dirname(fileURLToPath(import.meta.url))
|
|
29
|
+
|
|
30
|
+
function isPlainObject(value) {
|
|
31
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function isNonEmptyString(value) {
|
|
35
|
+
return typeof value === 'string' && value.length > 0
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function slotEntry(slot) {
|
|
39
|
+
return { id: `slot:${slot}`, type: slot }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Copy only the declared fields, so an entry carrying an unknown property never
|
|
43
|
+
// leaks into the normalized output and the input object is never mutated.
|
|
44
|
+
function copyEntry(entry) {
|
|
45
|
+
return {
|
|
46
|
+
id: entry.id,
|
|
47
|
+
type: entry.type,
|
|
48
|
+
...(isNonEmptyString(entry.variant) ? { variant: entry.variant } : {}),
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// A legacy `{ name, variant? }` entry (or its bare-string shorthand) — the
|
|
53
|
+
// pre-E3 shape that has no id/type yet.
|
|
54
|
+
export function isLegacySectionEntry(entry) {
|
|
55
|
+
const value = typeof entry === 'string' ? { name: entry } : entry
|
|
56
|
+
if (!isPlainObject(value)) return false
|
|
57
|
+
if (isNonEmptyString(value.id) || isNonEmptyString(value.type)) return false
|
|
58
|
+
return isNonEmptyString(value.name)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// A registered-slot entry: the reserved `id === "slot:" + type` form.
|
|
62
|
+
export function isSlotSectionEntry(entry) {
|
|
63
|
+
return (
|
|
64
|
+
isPlainObject(entry) &&
|
|
65
|
+
isNonEmptyString(entry.id) &&
|
|
66
|
+
isNonEmptyString(entry.type) &&
|
|
67
|
+
entry.id === `slot:${entry.type}`
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// An editorial instance: a persisted id that must not start with `slot:`.
|
|
72
|
+
export function isEditorialSectionEntry(entry) {
|
|
73
|
+
return (
|
|
74
|
+
isPlainObject(entry) &&
|
|
75
|
+
isNonEmptyString(entry.id) &&
|
|
76
|
+
isNonEmptyString(entry.type) &&
|
|
77
|
+
!entry.id.startsWith('slot:')
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Discriminate a declared entry: 'slot' (reserved id form), 'editorial'
|
|
82
|
+
// (persisted non-reserved id), 'legacy' (`{ name }`), or 'invalid'.
|
|
83
|
+
export function classifySectionEntry(entry) {
|
|
84
|
+
const value = typeof entry === 'string' ? { name: entry } : entry
|
|
85
|
+
if (!isPlainObject(value)) return 'invalid'
|
|
86
|
+
const hasId = isNonEmptyString(value.id)
|
|
87
|
+
const hasType = isNonEmptyString(value.type)
|
|
88
|
+
const hasName = isNonEmptyString(value.name)
|
|
89
|
+
if (hasId && hasType && !hasName) {
|
|
90
|
+
return value.id.startsWith('slot:') ? 'slot' : 'editorial'
|
|
91
|
+
}
|
|
92
|
+
if (hasName && !hasId && !hasType) return 'legacy'
|
|
93
|
+
return 'invalid'
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Normalize one view's declared sections to the ordered composition model.
|
|
98
|
+
*
|
|
99
|
+
* @param {string} viewName
|
|
100
|
+
* @param {unknown[]} declaredSections the manifest `views[].sections` (may be
|
|
101
|
+
* missing/empty, legacy, new-form, or a mix)
|
|
102
|
+
* @param {string[]} registeredSlots the view's ordered core slots
|
|
103
|
+
* @param {{ allowedSections?: string[], maxSections?: number }} [options]
|
|
104
|
+
* @returns {{ ok: boolean, sections: object[] | null, diagnostics: object[] }}
|
|
105
|
+
* On failure `sections` is null (no output) and `diagnostics` is non-empty.
|
|
106
|
+
*/
|
|
107
|
+
export function normalizeViewSections(viewName, declaredSections, registeredSlots, options = {}) {
|
|
108
|
+
const slots = Array.isArray(registeredSlots) ? registeredSlots.filter(isNonEmptyString) : []
|
|
109
|
+
const allowedSections = Array.isArray(options.allowedSections) ? options.allowedSections : null
|
|
110
|
+
const maxSections = typeof options.maxSections === 'number' ? options.maxSections : null
|
|
111
|
+
const declared = Array.isArray(declaredSections) ? declaredSections : []
|
|
112
|
+
|
|
113
|
+
const diagnostics = []
|
|
114
|
+
const push = (code, message, path) =>
|
|
115
|
+
diagnostics.push({ code, severity: 'error', message, ...(path ? { path } : {}) })
|
|
116
|
+
|
|
117
|
+
// Missing/empty composition: the registered slots in current render order and
|
|
118
|
+
// no editorial instance.
|
|
119
|
+
if (declared.length === 0) {
|
|
120
|
+
return { ok: true, sections: slots.map(slotEntry), diagnostics: [] }
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const emitted = []
|
|
124
|
+
const seenIds = new Set()
|
|
125
|
+
const emittedSlots = new Set()
|
|
126
|
+
const legacy = []
|
|
127
|
+
|
|
128
|
+
declared.forEach((raw, index) => {
|
|
129
|
+
const path = `views["${viewName}"].sections[${index}]`
|
|
130
|
+
const entry = typeof raw === 'string' ? { name: raw } : raw
|
|
131
|
+
const kind = classifySectionEntry(entry)
|
|
132
|
+
|
|
133
|
+
if (kind === 'slot') {
|
|
134
|
+
if (entry.id !== `slot:${entry.type}`) {
|
|
135
|
+
push(
|
|
136
|
+
'E_RESERVED_ID',
|
|
137
|
+
`${path} reserved slot id "${entry.id}" must equal "slot:${entry.type}".`,
|
|
138
|
+
path,
|
|
139
|
+
)
|
|
140
|
+
return
|
|
141
|
+
}
|
|
142
|
+
if (!slots.includes(entry.type)) {
|
|
143
|
+
push('E_UNKNOWN_HOST', `${path} names unregistered slot "${entry.type}".`, path)
|
|
144
|
+
return
|
|
145
|
+
}
|
|
146
|
+
if (emittedSlots.has(entry.type) || seenIds.has(entry.id)) {
|
|
147
|
+
push('E_DUPLICATE_ID', `${path} duplicate section id "${entry.id}".`, path)
|
|
148
|
+
return
|
|
149
|
+
}
|
|
150
|
+
emitted.push(copyEntry(entry))
|
|
151
|
+
emittedSlots.add(entry.type)
|
|
152
|
+
seenIds.add(entry.id)
|
|
153
|
+
return
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (kind === 'editorial') {
|
|
157
|
+
if (seenIds.has(entry.id)) {
|
|
158
|
+
push('E_DUPLICATE_ID', `${path} duplicate section id "${entry.id}".`, path)
|
|
159
|
+
return
|
|
160
|
+
}
|
|
161
|
+
if (allowedSections && !allowedSections.includes(entry.type)) {
|
|
162
|
+
push(
|
|
163
|
+
'E_DISALLOWED_TYPE',
|
|
164
|
+
`${path} section type "${entry.type}" is not an allowed section for view "${viewName}".`,
|
|
165
|
+
path,
|
|
166
|
+
)
|
|
167
|
+
return
|
|
168
|
+
}
|
|
169
|
+
emitted.push(copyEntry(entry))
|
|
170
|
+
seenIds.add(entry.id)
|
|
171
|
+
return
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (kind === 'legacy') {
|
|
175
|
+
legacy.push({ entry, path })
|
|
176
|
+
return
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
push(
|
|
180
|
+
'E_UNKNOWN_SECTION_SHAPE',
|
|
181
|
+
`${path} is neither a legacy { name } entry nor an { id, type } entry.`,
|
|
182
|
+
path,
|
|
183
|
+
)
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
// Every registered slot is mandatory: append any the declared list omitted, in
|
|
187
|
+
// registry order, so the normalized composition always contains each slot
|
|
188
|
+
// exactly once.
|
|
189
|
+
for (const slot of slots) {
|
|
190
|
+
if (!emittedSlots.has(slot)) {
|
|
191
|
+
const entry = slotEntry(slot)
|
|
192
|
+
emitted.push(entry)
|
|
193
|
+
emittedSlots.add(slot)
|
|
194
|
+
seenIds.add(entry.id)
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Persisted legacy migration: a concrete `<name>-<n>` id derived once from the
|
|
199
|
+
// 1-based position among the view's legacy entries, appended after the slots.
|
|
200
|
+
legacy.forEach(({ entry, path }, index) => {
|
|
201
|
+
const id = `${entry.name}-${index + 1}`
|
|
202
|
+
if (id.startsWith('slot:')) {
|
|
203
|
+
push(
|
|
204
|
+
'E_RESERVED_ID',
|
|
205
|
+
`${path} legacy name "${entry.name}" would mint a reserved id "${id}".`,
|
|
206
|
+
path,
|
|
207
|
+
)
|
|
208
|
+
return
|
|
209
|
+
}
|
|
210
|
+
if (allowedSections && !allowedSections.includes(entry.name)) {
|
|
211
|
+
push(
|
|
212
|
+
'E_DISALLOWED_TYPE',
|
|
213
|
+
`${path} legacy section "${entry.name}" is not an allowed section for view "${viewName}" (held or missing a verified bridge/provenance or data source); it cannot be adapted.`,
|
|
214
|
+
path,
|
|
215
|
+
)
|
|
216
|
+
return
|
|
217
|
+
}
|
|
218
|
+
if (seenIds.has(id)) {
|
|
219
|
+
push(
|
|
220
|
+
'E_DUPLICATE_ID',
|
|
221
|
+
`${path} legacy section "${entry.name}" would mint duplicate id "${id}".`,
|
|
222
|
+
path,
|
|
223
|
+
)
|
|
224
|
+
return
|
|
225
|
+
}
|
|
226
|
+
emitted.push({
|
|
227
|
+
id,
|
|
228
|
+
type: entry.name,
|
|
229
|
+
...(isNonEmptyString(entry.variant) ? { variant: entry.variant } : {}),
|
|
230
|
+
})
|
|
231
|
+
seenIds.add(id)
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
if (diagnostics.length > 0) {
|
|
235
|
+
return { ok: false, sections: null, diagnostics }
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const editorialCount = emitted.filter((entry) => isEditorialSectionEntry(entry)).length
|
|
239
|
+
if (maxSections !== null && editorialCount > maxSections) {
|
|
240
|
+
return {
|
|
241
|
+
ok: false,
|
|
242
|
+
sections: null,
|
|
243
|
+
diagnostics: [
|
|
244
|
+
{
|
|
245
|
+
code: 'E_SECTION_LIMIT',
|
|
246
|
+
severity: 'error',
|
|
247
|
+
message: `View "${viewName}" normalized to ${editorialCount} editorial instances, exceeding maxSections ${maxSections}.`,
|
|
248
|
+
},
|
|
249
|
+
],
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return { ok: true, sections: emitted, diagnostics: [] }
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Package-side seam over the view registry: resolve a view's `registeredSlots`,
|
|
257
|
+
// `allowedSections`, and `maxSections` and normalize its declared sections. The
|
|
258
|
+
// registry is read from the package (`recipes/`) or the installed-skill assets,
|
|
259
|
+
// so the caller passes only the view name and its declared list.
|
|
260
|
+
function resolveRegistryPath() {
|
|
261
|
+
const candidates = [
|
|
262
|
+
join(here, '..', 'recipes', 'recipes.json'),
|
|
263
|
+
join(here, 'recipes.json'),
|
|
264
|
+
join(here, '..', 'assets', 'recipes', 'recipes.json'),
|
|
265
|
+
]
|
|
266
|
+
return candidates.find((candidate) => existsSync(candidate)) ?? null
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
let cachedRecipes = null
|
|
270
|
+
|
|
271
|
+
function loadRecipes() {
|
|
272
|
+
if (!cachedRecipes) {
|
|
273
|
+
const path = resolveRegistryPath()
|
|
274
|
+
if (!path) throw new Error('section-compat: could not locate the view recipe registry.')
|
|
275
|
+
cachedRecipes = JSON.parse(readFileSync(path, 'utf8'))
|
|
276
|
+
}
|
|
277
|
+
return cachedRecipes
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
export function normalizeViewSectionsForView(viewName, declaredSections) {
|
|
282
|
+
const recipe = loadRecipes().views.find((view) => view.name === viewName) ?? null
|
|
283
|
+
if (!recipe) {
|
|
284
|
+
return {
|
|
285
|
+
ok: false,
|
|
286
|
+
sections: null,
|
|
287
|
+
diagnostics: [
|
|
288
|
+
{
|
|
289
|
+
code: 'E_UNKNOWN_VIEW',
|
|
290
|
+
severity: 'error',
|
|
291
|
+
message: `Unknown view "${viewName}".`,
|
|
292
|
+
},
|
|
293
|
+
],
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return normalizeViewSections(viewName, declaredSections, recipe.registeredSlots ?? [], {
|
|
297
|
+
allowedSections: recipe.allowedSections ?? [],
|
|
298
|
+
maxSections: recipe.maxSections,
|
|
299
|
+
})
|
|
300
|
+
}
|
|
@@ -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
|
+
}
|