@thorprovider/storefront 0.3.1 → 0.4.0-mvp.0
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/CHANGELOG.md +23 -0
- package/README.md +248 -99
- package/assets/templates/app/app/[[...slug]]/page.tsx +13 -2
- package/assets/templates/app/app/layout.tsx +18 -4
- package/assets/templates/app/app/robots.ts +13 -0
- package/assets/templates/app/app/sitemap.ts +12 -0
- package/assets/templates/app/lib/__STOREFRONT__/actions.ts +542 -0
- package/assets/templates/app/lib/__STOREFRONT__/cart-state.ts +66 -0
- package/assets/templates/app/lib/__STOREFRONT__/catalog.ts +40 -57
- package/assets/templates/app/lib/__STOREFRONT__/commerce.ts +608 -0
- package/assets/templates/app/lib/__STOREFRONT__/handlers.core.ts +165 -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 +232 -51
- package/assets/templates/app/lib/__STOREFRONT__/runtime.ts +3 -3
- package/assets/templates/app/lib/__STOREFRONT__/schema.ts +121 -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 +96 -0
- package/assets/templates/app/lib/__STOREFRONT__/shell/props.ts +34 -0
- package/assets/templates/app/lib/__STOREFRONT__/spec/index.ts +100 -11
- package/assets/templates/app/lib/__STOREFRONT__/spec/page-root.ts +5 -1
- package/assets/templates/app/lib/__STOREFRONT__/spec/seo.ts +26 -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 +142 -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 +17 -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/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/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 +153 -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 +36 -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 +378 -0
- package/assets/templates/views/account/schema.ts +52 -0
- package/assets/templates/views/category/assembly/assembly.json +28 -0
- package/assets/templates/views/category/assembly/index.skeleton.tsx +193 -0
- package/assets/templates/views/category/assembly/registry.skeleton.tsx +33 -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 +230 -0
- package/assets/templates/views/category/props.ts +16 -0
- package/assets/templates/views/category/registry.tsx +103 -0
- package/assets/templates/views/category/schema.ts +78 -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 +24 -0
- package/assets/templates/views/home/assembly/index.skeleton.tsx +72 -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 +15 -0
- package/assets/templates/views/home/index.tsx +38 -84
- 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 +48 -0
- package/assets/templates/views/product/index.tsx +104 -0
- package/assets/templates/views/product/props.ts +20 -0
- package/assets/templates/views/product/registry.tsx +78 -0
- package/assets/templates/views/product/schema.ts +108 -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/{skills/sf-view-gen/assets → assets}/templates/views/user-panel/index.tsx +8 -70
- package/{skills/sf-view-gen/assets → assets}/templates/views/user-panel/registry.tsx +23 -18
- package/assets/templates/views/user-panel/schema.ts +79 -0
- package/bin/storefront.js +1278 -79
- package/commands/sf-add-view.md +44 -13
- package/commands/sf-feature.md +28 -0
- package/commands/sf-theme.md +14 -8
- package/commands/sf-view.md +54 -12
- package/package.json +6 -4
- package/recipes/archetypes.json +28 -1
- package/recipes/catalog.json +361 -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 +139 -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 +60 -5
- package/recipes/sections.json +24 -5
- package/recipes/validate.mjs +784 -32
- package/scripts/candidate.mjs +236 -0
- package/scripts/composers.mjs +140 -0
- package/scripts/file-classes.mjs +116 -0
- package/scripts/logical-controls.mjs +61 -0
- package/scripts/manifest.mjs +298 -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 +504 -0
- package/scripts/plan-apply.mjs +843 -0
- package/scripts/preflight.mjs +51 -0
- package/scripts/reconcile.mjs +511 -0
- package/scripts/render-templates.mjs +245 -28
- package/scripts/schema-validate.mjs +132 -0
- package/scripts/section-compat.mjs +300 -0
- package/scripts/section-operations.mjs +508 -0
- package/scripts/sync-assets.mjs +22 -15
- package/scripts/view-assembly.mjs +291 -0
- package/scripts/view-selection.mjs +211 -0
- package/skills/sf-feature/SKILL.md +67 -0
- package/skills/sf-theme-gen/SKILL.md +12 -5
- package/skills/sf-view-gen/SKILL.md +123 -93
- package/skills/sf-view-gen/assets/recipes/archetypes.json +28 -1
- package/skills/sf-view-gen/assets/recipes/catalog.json +361 -0
- package/skills/sf-view-gen/assets/recipes/catalog.schema.json +64 -0
- package/skills/sf-view-gen/assets/recipes/features.json +139 -0
- package/skills/sf-view-gen/assets/recipes/recipes.json +60 -5
- package/skills/sf-view-gen/assets/recipes/sections.json +24 -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/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.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/sf-view-gen/references/views.md +44 -10
- 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 +784 -32
- package/skills/storefront-workflow/SKILL.md +116 -0
- 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
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
// The pinned candidate toolset for a generated external storefront.
|
|
2
|
+
//
|
|
3
|
+
// Exact versions only — the scaffolder (`create`), the generated project's
|
|
4
|
+
// runtime dependencies, and the local `@thorprovider/*` packages — so a fresh
|
|
5
|
+
// consumer installs a reproducible, compatible set instead of floating
|
|
6
|
+
// `latest`. The local `@thorprovider/*` packages are consumed from a packed
|
|
7
|
+
// local candidate (a directory of `*.tgz` tarballs) via Yarn `resolutions`
|
|
8
|
+
// while the declared dependency ranges stay honest semver, so M1 needs neither
|
|
9
|
+
// a publish nor a fake workspace import.
|
|
10
|
+
//
|
|
11
|
+
// The concrete versions are real (validated against the monorepo's own install
|
|
12
|
+
// and the npm registry); the actual external build is M1 evidence.
|
|
13
|
+
|
|
14
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs'
|
|
15
|
+
import { join } from 'node:path'
|
|
16
|
+
import { gunzipSync } from 'node:zlib'
|
|
17
|
+
|
|
18
|
+
// The exact compatible set. `runtime` are registry packages; `local` are the
|
|
19
|
+
// monorepo packages that must be packed for an external consumer.
|
|
20
|
+
export const CANDIDATE_TOOLSET = Object.freeze({
|
|
21
|
+
// The package manager the generated project pins in its `packageManager`
|
|
22
|
+
// field, so Corepack resolves the canonical Yarn instead of a floating
|
|
23
|
+
// "known good" release. Must match the monorepo root (drift-tested).
|
|
24
|
+
packageManager: 'yarn@4.5.1',
|
|
25
|
+
scaffolder: Object.freeze({
|
|
26
|
+
createNextApp: '16.2.3',
|
|
27
|
+
// shadcn 4.21.1 (not 4.21.2/4.21.3) pins @shadcn/registry to the
|
|
28
|
+
// already-published 0.1.0, so a fresh scaffold installs reproducibly past
|
|
29
|
+
// Yarn's npmMinimalAgeGate (the newest releases are age-gated).
|
|
30
|
+
shadcn: '4.21.1',
|
|
31
|
+
}),
|
|
32
|
+
runtime: Object.freeze({
|
|
33
|
+
next: '16.2.3',
|
|
34
|
+
react: '19.2.5',
|
|
35
|
+
'react-dom': '19.2.5',
|
|
36
|
+
'next-themes': '0.4.6',
|
|
37
|
+
'lucide-react': '1.47.0',
|
|
38
|
+
motion: '13.4.3',
|
|
39
|
+
zod: '3.25.76',
|
|
40
|
+
'@json-render/core': '0.21.0',
|
|
41
|
+
'@json-render/react': '0.21.0',
|
|
42
|
+
'@json-render/next': '0.21.0',
|
|
43
|
+
}),
|
|
44
|
+
local: Object.freeze({
|
|
45
|
+
'@thorprovider/blocks': '0.1.1',
|
|
46
|
+
'@thorprovider/adapters': '5.0.11',
|
|
47
|
+
'@thorprovider/runtime': '0.1.2',
|
|
48
|
+
'@thorprovider/types': '5.7.7',
|
|
49
|
+
}),
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
/** The monorepo package directories the `local` toolset versions must match. */
|
|
53
|
+
export const LOCAL_PACKAGE_DIRS = Object.freeze({
|
|
54
|
+
'@thorprovider/blocks': 'blocks',
|
|
55
|
+
'@thorprovider/adapters': 'adapters',
|
|
56
|
+
'@thorprovider/runtime': 'runtime',
|
|
57
|
+
'@thorprovider/types': 'types',
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
/** `@thorprovider/blocks` -> `thorprovider-blocks` (a `yarn pack` filename slug). */
|
|
61
|
+
export function packSlug(name) {
|
|
62
|
+
return name.replace(/^@/, '').replace(/\//g, '-')
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Find the local tarball for a package in a packs directory. Accepts the
|
|
67
|
+
* documented `yarn pack --filename <slug>-<version>.tgz` name, a plain
|
|
68
|
+
* `<slug>.tgz`, or any `.tgz` containing the slug.
|
|
69
|
+
*/
|
|
70
|
+
export function findPack(packsDir, name, version) {
|
|
71
|
+
if (!existsSync(packsDir)) return null
|
|
72
|
+
const slug = packSlug(name)
|
|
73
|
+
const named = [`${slug}-${version}.tgz`, `${slug}.tgz`]
|
|
74
|
+
for (const file of named) {
|
|
75
|
+
const abs = join(packsDir, file)
|
|
76
|
+
if (existsSync(abs)) return abs
|
|
77
|
+
}
|
|
78
|
+
for (const entry of readdirSync(packsDir).sort()) {
|
|
79
|
+
if (entry.endsWith('.tgz') && entry.includes(slug)) return join(packsDir, entry)
|
|
80
|
+
}
|
|
81
|
+
return null
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Build the exact install plan for a generated storefront.
|
|
86
|
+
* - `dependencies`: name -> exact version for every runtime + local package
|
|
87
|
+
* (plus the CLI itself), all honest semver.
|
|
88
|
+
* - `resolutions`: when `packsDir` is given, name -> `file:<abs>` mapping each
|
|
89
|
+
* local `@thorprovider/*` package (and the CLI) to its packed tarball.
|
|
90
|
+
* - `missingPacks`: local packages requested but absent from the packs dir.
|
|
91
|
+
*/
|
|
92
|
+
export function buildInstallPlan({ cliVersion, packsDir, toolset = CANDIDATE_TOOLSET } = {}) {
|
|
93
|
+
const dependencies = {
|
|
94
|
+
...toolset.runtime,
|
|
95
|
+
...toolset.local,
|
|
96
|
+
'@thorprovider/storefront': cliVersion,
|
|
97
|
+
}
|
|
98
|
+
const resolutions = {}
|
|
99
|
+
const missingPacks = []
|
|
100
|
+
if (packsDir) {
|
|
101
|
+
const local = { ...toolset.local, '@thorprovider/storefront': cliVersion }
|
|
102
|
+
for (const [name, version] of Object.entries(local)) {
|
|
103
|
+
const pack = findPack(packsDir, name, version)
|
|
104
|
+
if (!pack) missingPacks.push(name)
|
|
105
|
+
else resolutions[name] = `file:${pack}`
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return { dependencies, resolutions, missingPacks, scaffolder: toolset.scaffolder, packageManager: toolset.packageManager }
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Read the actual version of a sibling monorepo package (drift guard). */
|
|
112
|
+
export function readLocalPackageVersion(repoPackagesDir, name) {
|
|
113
|
+
const dir = LOCAL_PACKAGE_DIRS[name]
|
|
114
|
+
if (!dir) return null
|
|
115
|
+
const pkgPath = join(repoPackagesDir, dir, 'package.json')
|
|
116
|
+
if (!existsSync(pkgPath)) return null
|
|
117
|
+
return JSON.parse(readFileSync(pkgPath, 'utf8')).version
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Read the regular-file entries of a `.tgz` (gzipped tar) into a
|
|
122
|
+
* `path -> Buffer` map. A dependency-free reader (the candidate guard must run
|
|
123
|
+
* in a bare Node process, before any install). It understands the ustar header
|
|
124
|
+
* (name/size/type) and skips directories, symlinks, and pax/global headers —
|
|
125
|
+
* enough for a `yarn pack` tarball, whose paths are all short.
|
|
126
|
+
*/
|
|
127
|
+
export function readPackEntries(tgzPath) {
|
|
128
|
+
let raw
|
|
129
|
+
try {
|
|
130
|
+
raw = gunzipSync(readFileSync(tgzPath))
|
|
131
|
+
} catch {
|
|
132
|
+
return null
|
|
133
|
+
}
|
|
134
|
+
const entries = {}
|
|
135
|
+
let offset = 0
|
|
136
|
+
while (offset + 512 <= raw.length) {
|
|
137
|
+
const header = raw.subarray(offset, offset + 512)
|
|
138
|
+
// A zero block marks the end of the archive.
|
|
139
|
+
if (header.every((b) => b === 0)) break
|
|
140
|
+
const name = header.subarray(0, 100).toString('utf8').replace(/\0.*$/, '')
|
|
141
|
+
const sizeField = header.subarray(124, 136).toString('utf8').replace(/\0.*$/, '').trim()
|
|
142
|
+
const size = sizeField ? parseInt(sizeField, 8) : 0
|
|
143
|
+
const type = String.fromCharCode(header[156])
|
|
144
|
+
const dataStart = offset + 512
|
|
145
|
+
if (type === '0' || type === '\0' || type === '') {
|
|
146
|
+
entries[name] = raw.subarray(dataStart, dataStart + size)
|
|
147
|
+
}
|
|
148
|
+
offset = dataStart + Math.ceil(size / 512) * 512
|
|
149
|
+
}
|
|
150
|
+
return entries
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Parse `package/package.json` out of a packed tarball, or null. */
|
|
154
|
+
export function readPackManifest(tgzPath) {
|
|
155
|
+
const entries = readPackEntries(tgzPath)
|
|
156
|
+
const pkg = entries && entries['package/package.json']
|
|
157
|
+
if (!pkg) return null
|
|
158
|
+
try {
|
|
159
|
+
return JSON.parse(pkg.toString('utf8'))
|
|
160
|
+
} catch {
|
|
161
|
+
return null
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Guard a packed local candidate before it is trusted for an install: its
|
|
167
|
+
* declared `name` and `version` MUST match the pinned toolset. A stale tarball
|
|
168
|
+
* that kept its old version (or a wrong file matched by slug) is rejected here
|
|
169
|
+
* instead of producing a half-broken generated project.
|
|
170
|
+
*/
|
|
171
|
+
export function verifyPackManifest({ tgzPath, name, version }) {
|
|
172
|
+
if (!tgzPath || !existsSync(tgzPath)) {
|
|
173
|
+
return { ok: false, code: 'E_PACK_MISSING', message: `no packed candidate found for ${name}@${version}` }
|
|
174
|
+
}
|
|
175
|
+
const manifest = readPackManifest(tgzPath)
|
|
176
|
+
if (!manifest) {
|
|
177
|
+
return { ok: false, code: 'E_PACK_UNREADABLE', message: `cannot read ${tgzPath} as a package tarball` }
|
|
178
|
+
}
|
|
179
|
+
if (manifest.name !== name) {
|
|
180
|
+
return { ok: false, code: 'E_PACK_NAME_MISMATCH', message: `${tgzPath} declares ${manifest.name}, expected ${name}` }
|
|
181
|
+
}
|
|
182
|
+
if (manifest.version !== version) {
|
|
183
|
+
return {
|
|
184
|
+
ok: false,
|
|
185
|
+
code: 'E_PACK_VERSION_MISMATCH',
|
|
186
|
+
message: `${tgzPath} declares ${manifest.name}@${manifest.version}, expected ${version} — repack the candidate`,
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return { ok: true }
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Guard the runtime surface a generated host actually imports: every named
|
|
194
|
+
* import the storefront template pulls from `@thorprovider/runtime/commerce`
|
|
195
|
+
* MUST exist in the packed runtime build. This catches a same-version stale
|
|
196
|
+
* tarball whose source predates a new export (the failure a version bump alone
|
|
197
|
+
* cannot see).
|
|
198
|
+
*/
|
|
199
|
+
export function verifyPackExports({ tgzPath, innerPath, requiredExports }) {
|
|
200
|
+
const entries = readPackEntries(tgzPath)
|
|
201
|
+
if (!entries) {
|
|
202
|
+
return { ok: false, code: 'E_PACK_UNREADABLE', message: `cannot read ${tgzPath} as a package tarball` }
|
|
203
|
+
}
|
|
204
|
+
const file = entries[innerPath]
|
|
205
|
+
if (!file) {
|
|
206
|
+
return { ok: false, code: 'E_PACK_FILE_MISSING', message: `${tgzPath} is missing ${innerPath}` }
|
|
207
|
+
}
|
|
208
|
+
const source = file.toString('utf8')
|
|
209
|
+
const missing = requiredExports.filter((sym) => !new RegExp(`\\b${sym}\\b`).test(source))
|
|
210
|
+
if (missing.length > 0) {
|
|
211
|
+
return {
|
|
212
|
+
ok: false,
|
|
213
|
+
code: 'E_RUNTIME_SURFACE_MISMATCH',
|
|
214
|
+
message: `${tgzPath} is missing runtime export(s): ${missing.join(', ')} — repack the candidate`,
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return { ok: true }
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Extract the value-import names a template takes from
|
|
222
|
+
* `@thorprovider/runtime/commerce` (type-only imports are ignored — they
|
|
223
|
+
* compile away and need no runtime symbol).
|
|
224
|
+
*/
|
|
225
|
+
export function parseRuntimeCommerceImports(source) {
|
|
226
|
+
const block = source.match(/import\s*\{([\s\S]*?)\}\s*from\s*['"]@thorprovider\/runtime\/commerce['"]/)
|
|
227
|
+
if (!block) return []
|
|
228
|
+
return block[1]
|
|
229
|
+
.replace(/\/\/[^\n]*/g, '')
|
|
230
|
+
.split(',')
|
|
231
|
+
.map((entry) => entry.trim())
|
|
232
|
+
.filter(Boolean)
|
|
233
|
+
.filter((entry) => !/^type\s/.test(entry))
|
|
234
|
+
.map((entry) => entry.replace(/^type\s+/, '').split(/\s+as\s+/)[0].trim())
|
|
235
|
+
.filter(Boolean)
|
|
236
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
// Builders for the composition composers.
|
|
2
|
+
//
|
|
3
|
+
// `state.ts` and `handlers.ts` are not templates: they are generated from the
|
|
4
|
+
// manifest's feature list so that adding or removing a feature is a manifest
|
|
5
|
+
// edit plus a regenerate. Each installed feature ships a fragment under
|
|
6
|
+
// `features/<name>/`; the composer imports only the installed fragments.
|
|
7
|
+
|
|
8
|
+
/** `order-summary` -> `orderSummary`. */
|
|
9
|
+
export function toCamel(name) {
|
|
10
|
+
return name.replace(/-([a-z0-9])/g, (_, char) => char.toUpperCase())
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** `order-summary` -> `OrderSummary`. */
|
|
14
|
+
export function toPascal(name) {
|
|
15
|
+
return name
|
|
16
|
+
.split('-')
|
|
17
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
18
|
+
.join('')
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Build the `state.ts` composer: spread every installed feature's state slice
|
|
23
|
+
* over the core state. Re-exports the core module so views keep importing types.
|
|
24
|
+
*/
|
|
25
|
+
export function buildStateComposer(featureNames) {
|
|
26
|
+
const names = [...featureNames].sort()
|
|
27
|
+
const imports = names
|
|
28
|
+
.map((name) => `import { ${toCamel(name)}State } from './features/${name}/state'`)
|
|
29
|
+
.join('\n')
|
|
30
|
+
const spreads = names.map((name) => ` ...${toCamel(name)}State,`).join('\n')
|
|
31
|
+
return (
|
|
32
|
+
`// Generated by Thor Commerce storefront (composition composer: state).\n` +
|
|
33
|
+
`// Do not edit by hand — the manifest is the source of truth; run \`storefront sync\`.\n` +
|
|
34
|
+
`// Installed features contribute their state slices below.\n\n` +
|
|
35
|
+
`import { storefrontCoreState } from './state.core'\n` +
|
|
36
|
+
(imports ? `${imports}\n\n` : '\n') +
|
|
37
|
+
`export * from './state.core'\n\n` +
|
|
38
|
+
`export const storefrontInitialState = {\n` +
|
|
39
|
+
` ...storefrontCoreState,\n` +
|
|
40
|
+
(spreads ? `${spreads}\n` : '') +
|
|
41
|
+
`}\n`
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Build a feature's `adapter.ts`: a pure declaration of the commerce adapter
|
|
47
|
+
* capabilities the feature needs. The purchase implementation is owned by the
|
|
48
|
+
* commerce change; this file only records the structural dependency so the
|
|
49
|
+
* feature bundle is complete without it.
|
|
50
|
+
*/
|
|
51
|
+
export function buildFeatureAdapter(featureName, needs = []) {
|
|
52
|
+
const camel = toCamel(featureName)
|
|
53
|
+
const pascal = toPascal(featureName)
|
|
54
|
+
const list = needs.map((need) => `'${need}'`).join(', ')
|
|
55
|
+
return (
|
|
56
|
+
`// Generated by Thor Commerce storefront (feature: ${featureName} — adapter needs).\n` +
|
|
57
|
+
`// The commerce adapter capabilities this feature requires. The purchase\n` +
|
|
58
|
+
`// implementation is owned by the commerce change; this file only records the\n` +
|
|
59
|
+
`// structural dependency so the feature wiring is complete without it.\n\n` +
|
|
60
|
+
`export const ${camel}AdapterNeeds = [${list}] as const\n` +
|
|
61
|
+
`export type ${pascal}AdapterNeed = (typeof ${camel}AdapterNeeds)[number]\n`
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The catalog capability features the URL boundary understands. The generated
|
|
67
|
+
* `mountedFeatures` is the intersection of this set with the installed manifest
|
|
68
|
+
* features — never a hardcoded always-mounted list. The boundary's
|
|
69
|
+
* `CatalogFeatureName` union is the type authority for this set.
|
|
70
|
+
*/
|
|
71
|
+
const CATALOG_FEATURE_NAMES = ['catalog-search', 'category-filters']
|
|
72
|
+
|
|
73
|
+
function isPlainObject(value) {
|
|
74
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Build the `composition.ts` composer: the installed manifest catalog features
|
|
79
|
+
* in stable order, plus their persisted config. This is the generation seam the
|
|
80
|
+
* catalog URL boundary consumes to reject parameters owned by a feature that is
|
|
81
|
+
* not installed, and the seam the single controlled `CategoryFilter2` reads to
|
|
82
|
+
* select its control visibility/config from the installed manifest only. It is
|
|
83
|
+
* derived from the manifest — never hardcoded as always-mounted.
|
|
84
|
+
*
|
|
85
|
+
* `features` accepts bare names or `{ name, config }` entries (the fresh
|
|
86
|
+
* scaffold and a later reconcile both pass manifest entries).
|
|
87
|
+
*/
|
|
88
|
+
export function buildCompositionComposer(features) {
|
|
89
|
+
const entries = (Array.isArray(features) ? features : []).map((feature) =>
|
|
90
|
+
typeof feature === 'string' ? { name: feature, config: {} } : feature,
|
|
91
|
+
)
|
|
92
|
+
const installed = new Set(entries.map((entry) => entry.name))
|
|
93
|
+
const names = CATALOG_FEATURE_NAMES.filter((name) => installed.has(name))
|
|
94
|
+
const list = names.map((name) => JSON.stringify(name)).join(', ')
|
|
95
|
+
const configLines = names
|
|
96
|
+
.map((name) => {
|
|
97
|
+
const entry = entries.find((candidate) => candidate.name === name)
|
|
98
|
+
const config = entry && isPlainObject(entry.config) ? entry.config : {}
|
|
99
|
+
return ` ${JSON.stringify(name)}: ${JSON.stringify(config)},`
|
|
100
|
+
})
|
|
101
|
+
.join('\n')
|
|
102
|
+
return (
|
|
103
|
+
`// Generated by Thor Commerce storefront (composition composer: features).\n` +
|
|
104
|
+
`// Do not edit by hand — the manifest is the source of truth; run \`storefront sync\`.\n` +
|
|
105
|
+
`// Installed manifest catalog features, in stable order, with their persisted\n` +
|
|
106
|
+
`// config. This is the generation seam the catalog URL boundary consumes to\n` +
|
|
107
|
+
`// reject parameters owned by a feature that is not installed, and the seam the\n` +
|
|
108
|
+
`// single controlled CategoryFilter2 reads to select its control\n` +
|
|
109
|
+
`// visibility/config from the installed manifest only.\n\n` +
|
|
110
|
+
`import type { CatalogFeatureName } from '@thorprovider/runtime/contracts'\n\n` +
|
|
111
|
+
`export const mountedFeatures: readonly CatalogFeatureName[] = [${list}]\n\n` +
|
|
112
|
+
`export const mountedFeatureConfigs: Partial<Record<CatalogFeatureName, Record<string, unknown>>> = {\n` +
|
|
113
|
+
`${configLines ? `${configLines}\n` : ''}` +
|
|
114
|
+
`}\n`
|
|
115
|
+
)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Build the `handlers.ts` composer: call the core handlers hook plus every
|
|
120
|
+
* installed feature's handlers hook.
|
|
121
|
+
*/
|
|
122
|
+
export function buildHandlersComposer(featureNames) {
|
|
123
|
+
const names = [...featureNames].sort()
|
|
124
|
+
const imports = names
|
|
125
|
+
.map((name) => `import { use${toPascal(name)}Handlers } from './features/${name}/handlers'`)
|
|
126
|
+
.join('\n')
|
|
127
|
+
const calls = names.map((name) => ` use${toPascal(name)}Handlers()`).join('\n')
|
|
128
|
+
return (
|
|
129
|
+
`'use client'\n\n` +
|
|
130
|
+
`// Generated by Thor Commerce storefront (composition composer: handlers).\n` +
|
|
131
|
+
`// Do not edit by hand — the manifest is the source of truth; run \`storefront sync\`.\n` +
|
|
132
|
+
`// Installed features contribute their handlers below.\n\n` +
|
|
133
|
+
`import { useCoreHandlers } from './handlers.core'\n` +
|
|
134
|
+
(imports ? `${imports}\n\n` : '\n') +
|
|
135
|
+
`export function useStorefrontHandlers() {\n` +
|
|
136
|
+
` useCoreHandlers()\n` +
|
|
137
|
+
(calls ? `${calls}\n` : '') +
|
|
138
|
+
`}\n`
|
|
139
|
+
)
|
|
140
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// File classification for reconcile.
|
|
2
|
+
//
|
|
3
|
+
// Every file the workflow touches is exactly one of:
|
|
4
|
+
// - external — create-next-app / shadcn base; only idempotent marker appends
|
|
5
|
+
// - user-owned — content, props, copy, local schemas, mapper bodies; never rewritten
|
|
6
|
+
// - derived — regenerated from the manifest on every reconcile
|
|
7
|
+
//
|
|
8
|
+
// Paths are relative to the template root (assets/templates), using forward slashes.
|
|
9
|
+
|
|
10
|
+
import { readdirSync } from 'node:fs'
|
|
11
|
+
import { join, relative, sep } from 'node:path'
|
|
12
|
+
|
|
13
|
+
export const FILE_CLASSES = {
|
|
14
|
+
derived: 'derived',
|
|
15
|
+
userOwned: 'user-owned',
|
|
16
|
+
external: 'external',
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const RULES = [
|
|
20
|
+
{
|
|
21
|
+
class: FILE_CLASSES.external,
|
|
22
|
+
patterns: [
|
|
23
|
+
'app/app/layout.tsx',
|
|
24
|
+
'app/app/globals.css',
|
|
25
|
+
'app/app/sitemap.ts',
|
|
26
|
+
'app/app/robots.ts',
|
|
27
|
+
],
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
class: FILE_CLASSES.userOwned,
|
|
31
|
+
patterns: [
|
|
32
|
+
'views/*/schema.ts',
|
|
33
|
+
'views/*/content.ts',
|
|
34
|
+
'views/*/props.ts',
|
|
35
|
+
'views/*/data.ts',
|
|
36
|
+
'app/lib/*/mappers.ts',
|
|
37
|
+
'app/lib/*/icon-map.ts',
|
|
38
|
+
// The shell brand/newsletter seam is user-owned so its edits survive sync.
|
|
39
|
+
'app/lib/*/shell/**',
|
|
40
|
+
],
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
class: FILE_CLASSES.derived,
|
|
44
|
+
patterns: [
|
|
45
|
+
'app/app/[[...slug]]/**',
|
|
46
|
+
'app/lib/*/handlers.ts',
|
|
47
|
+
'app/lib/*/handlers.core.ts',
|
|
48
|
+
'app/lib/*/state.ts',
|
|
49
|
+
'app/lib/*/state.core.ts',
|
|
50
|
+
'app/lib/*/composition.ts',
|
|
51
|
+
'app/lib/*/session.ts',
|
|
52
|
+
'app/lib/*/session.core.ts',
|
|
53
|
+
'app/lib/*/store-context.ts',
|
|
54
|
+
'app/lib/*/commerce.ts',
|
|
55
|
+
'app/lib/*/cart-state.ts',
|
|
56
|
+
'app/lib/*/actions.ts',
|
|
57
|
+
'app/lib/*/catalog.ts',
|
|
58
|
+
'app/lib/*/runtime.ts',
|
|
59
|
+
'app/lib/*/registry.tsx',
|
|
60
|
+
'app/lib/*/schema.ts',
|
|
61
|
+
'app/lib/*/spec/**',
|
|
62
|
+
'app/lib/*/views/**',
|
|
63
|
+
'features/**',
|
|
64
|
+
'views/*/index.tsx',
|
|
65
|
+
'views/*/registry.tsx',
|
|
66
|
+
// The views-root barrels are generated by reconcile (buildBarrel /
|
|
67
|
+
// buildClientRegistryBarrel), not copied from a view template. Their
|
|
68
|
+
// canonical template paths are app/lib/*/views/{index,registry}.ts, but
|
|
69
|
+
// projectIdentity maps a project's lib/<name>/views/* to the views/
|
|
70
|
+
// namespace, so the generated barrels must be classified here too or they
|
|
71
|
+
// default to user-owned in the identity domain (E1 task 4.4).
|
|
72
|
+
'views/index.ts',
|
|
73
|
+
'views/registry.ts',
|
|
74
|
+
// Per-slot/per-variant assembly sources compose the derived index/registry;
|
|
75
|
+
// they are authoring input and are never copied into a project.
|
|
76
|
+
'views/*/assembly/**',
|
|
77
|
+
],
|
|
78
|
+
},
|
|
79
|
+
].map((rule) => ({ class: rule.class, regexes: rule.patterns.map(globToRegExp) }))
|
|
80
|
+
|
|
81
|
+
function globToRegExp(glob) {
|
|
82
|
+
const placeholders = glob.replace(/\*\*/g, '\u0001').replace(/\*/g, '\u0002')
|
|
83
|
+
const escaped = placeholders.replace(/[.+^${}()|[\]\\?]/g, '\\$&')
|
|
84
|
+
const source = escaped.replace(/\u0001/g, '.*').replace(/\u0002/g, '[^/]*')
|
|
85
|
+
return new RegExp(`^${source}$`)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function normalize(relPath) {
|
|
89
|
+
return relPath.split(sep).join('/')
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Every class whose pattern matches the path. Well-formed template sets yield
|
|
94
|
+
* exactly one; more than one signals overlapping rules.
|
|
95
|
+
*/
|
|
96
|
+
export function classifyFileAll(relPath) {
|
|
97
|
+
const path = normalize(relPath)
|
|
98
|
+
return RULES.filter((rule) => rule.regexes.some((re) => re.test(path))).map((rule) => rule.class)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function classifyFile(relPath) {
|
|
102
|
+
return classifyFileAll(relPath)[0] ?? null
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function listTemplateFiles(root) {
|
|
106
|
+
const files = []
|
|
107
|
+
const walk = (dir) => {
|
|
108
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
109
|
+
const full = join(dir, entry.name)
|
|
110
|
+
if (entry.isDirectory()) walk(full)
|
|
111
|
+
else files.push(normalize(relative(root, full)))
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
walk(root)
|
|
115
|
+
return files.sort()
|
|
116
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// E2 task 6.1 (CU-11): the catalog search/filter logical-control metadata.
|
|
3
|
+
//
|
|
4
|
+
// WHY THIS EXISTS: `scripts/operation-registry.mjs` is E1's additive seam for
|
|
5
|
+
// logical-control metadata (a control slot that contributes to one already
|
|
6
|
+
// rendered presentation). E1 registers none in production; E2 owns the two
|
|
7
|
+
// catalog controls and registers them here. Generation consumes this registry
|
|
8
|
+
// through `logicalControlFor(registry, host, slot)` so a mount registered as a
|
|
9
|
+
// logical control is a contribution to the host's single presentation — not a
|
|
10
|
+
// variant swap — and the presentation/variant requirement is skipped for exactly
|
|
11
|
+
// those mounts (E2 design Decision 2c / the task 6.3 M2 resolution).
|
|
12
|
+
//
|
|
13
|
+
// This is metadata only: no new operation, no block, no element. The base
|
|
14
|
+
// listing (`category.filter` rendering the one controlled `CategoryFilter2`)
|
|
15
|
+
// stays the view default; these controls only toggle visibility on it.
|
|
16
|
+
//
|
|
17
|
+
// IMPORT-CYCLE NOTE: this module is a LEAF. It deliberately does not import
|
|
18
|
+
// `operations.mjs` (which reaches `view-selection.mjs` through
|
|
19
|
+
// `preflight.mjs -> render-templates.mjs`); the generation lookup only needs the
|
|
20
|
+
// logical-control metadata, not E1's operation vocabulary. The full production
|
|
21
|
+
// registry (E1's six operations + these controls) is built in `operations.mjs`.
|
|
22
|
+
|
|
23
|
+
import {
|
|
24
|
+
LOGICAL_CONTROL_KIND,
|
|
25
|
+
createOperationRegistry,
|
|
26
|
+
logicalControlFor,
|
|
27
|
+
} from './operation-registry.mjs'
|
|
28
|
+
|
|
29
|
+
/** The two E2 control contributions to the single `CategoryFilter2` listing. */
|
|
30
|
+
export const CATALOG_LOGICAL_CONTROLS = Object.freeze([
|
|
31
|
+
Object.freeze({
|
|
32
|
+
kind: LOGICAL_CONTROL_KIND,
|
|
33
|
+
feature: 'catalog-search',
|
|
34
|
+
host: 'category',
|
|
35
|
+
slot: 'search',
|
|
36
|
+
presentation: 'CategoryFilter2',
|
|
37
|
+
capability: 'category.search.apply',
|
|
38
|
+
}),
|
|
39
|
+
Object.freeze({
|
|
40
|
+
kind: LOGICAL_CONTROL_KIND,
|
|
41
|
+
feature: 'category-filters',
|
|
42
|
+
host: 'category',
|
|
43
|
+
slot: 'filters',
|
|
44
|
+
presentation: 'CategoryFilter2',
|
|
45
|
+
capability: 'category.filter.apply',
|
|
46
|
+
}),
|
|
47
|
+
])
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* A controls-only registry built from the same additive seam. Generation reads
|
|
51
|
+
* it through `logicalControlForMount`; the operation vocabulary is irrelevant to
|
|
52
|
+
* that lookup, so this registry carries no operations.
|
|
53
|
+
*/
|
|
54
|
+
export const CATALOG_LOGICAL_CONTROL_REGISTRY = createOperationRegistry({
|
|
55
|
+
logicalControls: CATALOG_LOGICAL_CONTROLS,
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
/** The logical control registered for a host slot, or null. */
|
|
59
|
+
export function logicalControlForMount(host, slot) {
|
|
60
|
+
return logicalControlFor(CATALOG_LOGICAL_CONTROL_REGISTRY, host, slot)
|
|
61
|
+
}
|