create-nymbal-app 0.4.0 → 0.5.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/dist/index.d.ts +2 -1
- package/dist/index.js +7 -4
- package/dist/index.js.map +1 -1
- package/package.json +5 -4
- package/scaffold/project/astro/src/pages/index.astro +13 -15
- package/scaffold/templates/astro/_dot.node-version +1 -1
- package/scaffold/templates/astro/astro.config.mjs +23 -0
- package/scaffold/templates/astro/package.json +26 -26
- package/scaffold/templates/astro/src/components/SkipNav.astro +3 -1
- package/scaffold/templates/astro/src/elements/index.ts +11 -0
- package/scaffold/templates/astro/src/layouts/BaseLayout.astro +15 -4
- package/scaffold/templates/astro/src/lib/api.ts +113 -21
- package/scaffold/templates/astro/src/lib/chrome.ts +59 -0
- package/scaffold/templates/astro/src/lib/client.ts +4 -4
- package/scaffold/templates/astro/src/lib/fragments.ts +48 -0
- package/scaffold/templates/astro/src/lib/server-elements.ts +5 -0
- package/scaffold/templates/astro/src/middleware.ts +37 -0
- package/scaffold/templates/astro/src/pages/account/addresses.astro +5 -97
- package/scaffold/templates/astro/src/pages/account/login.astro +7 -34
- package/scaffold/templates/astro/src/pages/account/profile.astro +6 -56
- package/scaffold/templates/astro/src/pages/account/register.astro +7 -73
- package/scaffold/templates/astro/src/pages/cart.astro +14 -7
- package/scaffold/templates/astro/src/pages/category/[slug].astro +17 -21
- package/scaffold/templates/astro/src/pages/checkout.astro +30 -94
- package/scaffold/templates/astro/src/pages/fragments/footer.astro +5 -2
- package/scaffold/templates/astro/src/pages/fragments/header.astro +12 -2
- package/scaffold/templates/astro/src/pages/fragments/nav.astro +8 -5
- package/scaffold/templates/astro/src/pages/fragments/product/[slug].astro +9 -10
- package/scaffold/templates/astro/src/pages/index.astro +18 -15
- package/scaffold/templates/astro/src/pages/order/[orderNumber].astro +22 -44
- package/scaffold/templates/astro/src/pages/product/[slug].astro +44 -30
- package/scaffold/templates/astro/src/pages/products/index.astro +16 -21
- package/scaffold/templates/astro/src/pages/search.astro +10 -11
- package/scaffold/templates/astro/src/scripts/init-client.ts +14 -0
- package/scaffold/templates/astro/src/scripts/pdp.ts +24 -21
- package/scaffold/templates/astro/src/scripts/plp.ts +15 -93
- package/scaffold/templates/astro/src/scripts/search.ts +7 -132
- package/scaffold/templates/astro/src/scripts/signed-in-redirect.ts +4 -0
- package/scaffold/templates/astro/src/styles/global.css +49 -45
- package/scaffold/templates/astro/src/styles/pages/account.css +0 -90
- package/scaffold/templates/astro/src/styles/pages/confirmation.css +6 -0
- package/scaffold/templates/astro/src/styles/pages/home.css +9 -100
- package/scaffold/templates/astro/src/styles/pages/pdp.css +5 -171
- package/scaffold/templates/astro/src/styles/pages/plp.css +3 -42
- package/scaffold/templates/astro/src/styles/pages/search.css +4 -110
- package/scaffold/templates/astro/src/styles/tokens.css +56 -0
- package/scaffold/templates/nextjs/_dot.node-version +1 -1
- package/scaffold/templates/nextjs/app/(catalog)/category/[slug]/page.tsx +74 -0
- package/scaffold/templates/nextjs/app/(catalog)/home/[key]/page.tsx +80 -0
- package/scaffold/templates/nextjs/app/(catalog)/layout.tsx +13 -0
- package/scaffold/templates/nextjs/app/(catalog)/not-found.tsx +7 -0
- package/scaffold/templates/nextjs/app/(catalog)/product/[slug]/page.tsx +182 -0
- package/scaffold/templates/nextjs/app/(catalog)/products/page.tsx +62 -0
- package/scaffold/templates/nextjs/app/(catalog)/search/SearchView.tsx +90 -0
- package/scaffold/templates/nextjs/app/(catalog)/search/page.tsx +46 -0
- package/scaffold/templates/nextjs/app/(shopper)/account/addresses/AddressesView.tsx +5 -0
- package/scaffold/templates/nextjs/app/(shopper)/account/addresses/page.tsx +17 -0
- package/scaffold/templates/nextjs/app/(shopper)/account/login/LoginView.tsx +28 -0
- package/scaffold/templates/nextjs/app/(shopper)/account/login/page.tsx +23 -0
- package/scaffold/templates/nextjs/app/(shopper)/account/profile/ProfileView.tsx +5 -0
- package/scaffold/templates/nextjs/app/(shopper)/account/profile/page.tsx +20 -0
- package/scaffold/templates/nextjs/app/(shopper)/account/register/RegisterView.tsx +26 -0
- package/scaffold/templates/nextjs/app/(shopper)/account/register/page.tsx +11 -0
- package/scaffold/templates/nextjs/app/(shopper)/cart/CartView.tsx +5 -0
- package/scaffold/templates/nextjs/app/(shopper)/cart/page.tsx +22 -0
- package/scaffold/templates/nextjs/app/(shopper)/checkout/CheckoutView.tsx +27 -0
- package/scaffold/templates/nextjs/app/(shopper)/checkout/page.tsx +39 -0
- package/scaffold/templates/nextjs/app/(shopper)/layout.tsx +12 -0
- package/scaffold/templates/nextjs/app/{order → (shopper)/order}/[orderNumber]/page.tsx +24 -5
- package/scaffold/templates/nextjs/app/error.tsx +3 -7
- package/scaffold/templates/nextjs/app/fragments/footer/route.ts +13 -0
- package/scaffold/templates/nextjs/app/fragments/header/[key]/route.ts +18 -0
- package/scaffold/templates/nextjs/app/fragments/nav/[key]/route.ts +23 -0
- package/scaffold/templates/nextjs/app/fragments/product/[slug]/route.ts +22 -0
- package/scaffold/templates/nextjs/app/globals.css +5 -23
- package/scaffold/templates/nextjs/app/layout.tsx +8 -5
- package/scaffold/templates/nextjs/app/not-found.tsx +9 -33
- package/scaffold/templates/nextjs/app/tokens.css +56 -0
- package/scaffold/templates/nextjs/components/client/ListingControls.tsx +87 -0
- package/scaffold/templates/nextjs/components/client/NymbalSetup.tsx +48 -6
- package/scaffold/templates/nextjs/components/client/ProductInteractive.tsx +30 -15
- package/scaffold/templates/nextjs/components/client/elements.tsx +15 -0
- package/scaffold/templates/nextjs/components/server/Header.tsx +33 -102
- package/scaffold/templates/nextjs/components/server/NotFoundView.tsx +39 -0
- package/scaffold/templates/nextjs/components/server/SkipNav.tsx +7 -3
- package/scaffold/templates/nextjs/elements/index.ts +12 -0
- package/scaffold/templates/nextjs/lib/api.ts +151 -55
- package/scaffold/templates/nextjs/lib/chrome.ts +40 -0
- package/scaffold/templates/nextjs/lib/fragments.ts +16 -0
- package/scaffold/templates/nextjs/lib/header.ts +39 -0
- package/scaffold/templates/nextjs/lib/render.ts +14 -0
- package/scaffold/templates/nextjs/next.config.mjs +32 -0
- package/scaffold/templates/nextjs/package.json +28 -27
- package/scaffold/templates/nextjs/styles/pages/account.module.css +0 -70
- package/scaffold/templates/nextjs/styles/pages/confirmation.module.css +6 -85
- package/scaffold/templates/nextjs/styles/pages/home.module.css +2 -103
- package/scaffold/templates/nextjs/styles/pages/pdp.module.css +3 -183
- package/scaffold/templates/nextjs/styles/pages/plp.module.css +9 -143
- package/scaffold/templates/nextjs/styles/pages/search.module.css +1 -145
- package/scaffold/templates/nextjs/tsconfig.dist.json +5 -0
- package/scaffold/templates/astro/src/components/AddressForm.astro +0 -107
- package/scaffold/templates/astro/src/components/Breadcrumb.astro +0 -20
- package/scaffold/templates/astro/src/components/CategoryNav.astro +0 -27
- package/scaffold/templates/astro/src/components/CheckoutSteps.astro +0 -29
- package/scaffold/templates/astro/src/components/Footer.astro +0 -108
- package/scaffold/templates/astro/src/components/Header.astro +0 -198
- package/scaffold/templates/astro/src/components/ImageGallery.astro +0 -42
- package/scaffold/templates/astro/src/components/OrderSummary.astro +0 -53
- package/scaffold/templates/astro/src/components/Pagination.astro +0 -13
- package/scaffold/templates/astro/src/components/PriceDisplay.astro +0 -25
- package/scaffold/templates/astro/src/components/ProductCard.astro +0 -44
- package/scaffold/templates/astro/src/components/RelatedProducts.astro +0 -28
- package/scaffold/templates/astro/src/components/ReviewForm.astro +0 -74
- package/scaffold/templates/astro/src/components/ReviewList.astro +0 -41
- package/scaffold/templates/astro/src/components/StockBadge.astro +0 -15
- package/scaffold/templates/astro/src/scripts/auth-forms.ts +0 -82
- package/scaffold/templates/astro/src/scripts/cart-page.ts +0 -118
- package/scaffold/templates/astro/src/scripts/checkout.ts +0 -271
- package/scaffold/templates/astro/src/scripts/image-gallery.ts +0 -22
- package/scaffold/templates/astro/src/scripts/profile-form.ts +0 -57
- package/scaffold/templates/astro/src/scripts/review-form.ts +0 -27
- package/scaffold/templates/astro/src/styles/pages/cart.css +0 -215
- package/scaffold/templates/astro/src/styles/pages/checkout.css +0 -270
- package/scaffold/templates/nextjs/app/account/addresses/page.tsx +0 -10
- package/scaffold/templates/nextjs/app/account/login/page.tsx +0 -23
- package/scaffold/templates/nextjs/app/account/profile/page.tsx +0 -10
- package/scaffold/templates/nextjs/app/account/register/page.tsx +0 -10
- package/scaffold/templates/nextjs/app/cart/page.tsx +0 -10
- package/scaffold/templates/nextjs/app/category/[slug]/page.tsx +0 -61
- package/scaffold/templates/nextjs/app/checkout/page.tsx +0 -17
- package/scaffold/templates/nextjs/app/page.tsx +0 -63
- package/scaffold/templates/nextjs/app/product/[slug]/page.tsx +0 -111
- package/scaffold/templates/nextjs/app/products/page.tsx +0 -40
- package/scaffold/templates/nextjs/app/search/page.tsx +0 -24
- package/scaffold/templates/nextjs/components/client/AddressManager.tsx +0 -181
- package/scaffold/templates/nextjs/components/client/CartPage.tsx +0 -121
- package/scaffold/templates/nextjs/components/client/CheckoutForm.tsx +0 -482
- package/scaffold/templates/nextjs/components/client/ImageGallery.tsx +0 -68
- package/scaffold/templates/nextjs/components/client/LoginForm.tsx +0 -89
- package/scaffold/templates/nextjs/components/client/MiniCartWrapper.tsx +0 -12
- package/scaffold/templates/nextjs/components/client/ProductGrid.tsx +0 -137
- package/scaffold/templates/nextjs/components/client/ProfileForm.tsx +0 -112
- package/scaffold/templates/nextjs/components/client/RegisterForm.tsx +0 -131
- package/scaffold/templates/nextjs/components/client/ReviewForm.tsx +0 -116
- package/scaffold/templates/nextjs/components/client/SearchBarWrapper.tsx +0 -27
- package/scaffold/templates/nextjs/components/client/SearchResults.tsx +0 -181
- package/scaffold/templates/nextjs/components/server/Breadcrumb.tsx +0 -36
- package/scaffold/templates/nextjs/components/server/CategoryNav.tsx +0 -29
- package/scaffold/templates/nextjs/components/server/CheckoutSteps.tsx +0 -60
- package/scaffold/templates/nextjs/components/server/Footer.module.css +0 -64
- package/scaffold/templates/nextjs/components/server/Footer.tsx +0 -79
- package/scaffold/templates/nextjs/components/server/Header.module.css +0 -147
- package/scaffold/templates/nextjs/components/server/OrderSummary.tsx +0 -79
- package/scaffold/templates/nextjs/components/server/PriceDisplay.tsx +0 -31
- package/scaffold/templates/nextjs/components/server/ProductCard.tsx +0 -59
- package/scaffold/templates/nextjs/components/server/RelatedProducts.tsx +0 -31
- package/scaffold/templates/nextjs/components/server/ReviewList.tsx +0 -62
- package/scaffold/templates/nextjs/styles/pages/cart.module.css +0 -240
- package/scaffold/templates/nextjs/styles/pages/checkout.module.css +0 -404
- /package/scaffold/templates/nextjs/app/{account → (shopper)/account}/layout.tsx +0 -0
- /package/scaffold/templates/nextjs/app/{account → (shopper)/account}/orders/[orderNumber]/page.tsx +0 -0
- /package/scaffold/templates/nextjs/app/{account → (shopper)/account}/orders/page.tsx +0 -0
- /package/scaffold/templates/nextjs/app/{account → (shopper)/account}/page.tsx +0 -0
package/dist/index.d.ts
CHANGED
|
@@ -34,4 +34,5 @@ declare function parseCliFlags(argv: string[]): CliFlags;
|
|
|
34
34
|
declare function main(argv?: string[], deps?: MainDeps): Promise<void>;
|
|
35
35
|
declare function scaffold(dir: string, answers: Answers): Promise<void>;
|
|
36
36
|
|
|
37
|
-
export { CliFlagError,
|
|
37
|
+
export { CliFlagError, detectPackageManager, main, parseCliFlags, scaffold };
|
|
38
|
+
export type { CliFlags, MainDeps, PackageManager, Runner };
|
package/dist/index.js
CHANGED
|
@@ -18,6 +18,9 @@ async function writeAdminEnv(dir, admin) {
|
|
|
18
18
|
"# The admin account the seed creates. Keep this file out of version control (.gitignore lists it).",
|
|
19
19
|
`NYMBAL_ADMIN_EMAIL=${admin.email}`,
|
|
20
20
|
`NYMBAL_ADMIN_PASSWORD=${admin.password}`,
|
|
21
|
+
"",
|
|
22
|
+
"# Encrypts the keys saved in the admin's Settings. Losing it loses them: back it up with your deploy's secrets.",
|
|
23
|
+
`NYMBAL_SECRETS_KEY=${randomBytes(32).toString("base64")}`,
|
|
21
24
|
""
|
|
22
25
|
];
|
|
23
26
|
await writeFile(resolve(dir, ".env"), lines.join("\n"), { encoding: "utf8", mode: 384 });
|
|
@@ -343,10 +346,10 @@ export default defineConfig({
|
|
|
343
346
|
security: {
|
|
344
347
|
adapter: 'middleware',
|
|
345
348
|
rateLimit: {
|
|
346
|
-
'/checkout': { requests: 10, window: '
|
|
347
|
-
'/auth/*': { requests: 5, window: '
|
|
348
|
-
'/api/*': { requests: 100, window: '
|
|
349
|
-
'/webhooks/*': { requests: 200, window: '
|
|
349
|
+
'/checkout': { requests: 10, window: '1m', action: 'block' },
|
|
350
|
+
'/auth/*': { requests: 5, window: '1m', action: 'challenge' },
|
|
351
|
+
'/api/*': { requests: 100, window: '1m', action: 'throttle' },
|
|
352
|
+
'/webhooks/*': { requests: 200, window: '1m', action: 'throttle' },
|
|
350
353
|
},
|
|
351
354
|
botProtection: { mode: 'managed', allowList: ['googlebot', 'bingbot', 'stripe-webhooks'] },
|
|
352
355
|
headers: {
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["import { cp, readFile, mkdir, readdir, rm, writeFile } from 'node:fs/promises'\nimport { existsSync } from 'node:fs'\nimport { dirname, resolve } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport { spawn, spawnSync } from 'node:child_process'\nimport { randomBytes } from 'node:crypto'\nimport { cancel, confirm, intro, isCancel, note, outro, select, text } from '@clack/prompts'\nimport pc from 'picocolors'\n\nconst HERE = dirname(fileURLToPath(import.meta.url))\nconst SCAFFOLD_ROOT = resolve(HERE, '..', 'scaffold')\n\nexport type PackageManager = 'npm' | 'pnpm' | 'yarn-classic' | 'yarn-berry' | 'bun'\n\ninterface Answers {\n name: string\n directory: string\n currency: 'GBP' | 'USD' | 'EUR'\n template: 'astro' | 'nextjs'\n seedDemo: boolean\n}\n\n/**\n * Runs a command in `cwd` (with `env` when given, else the process environment) and rejects on a non-zero exit.\n * Injected into `main` so tests do not shell out.\n */\nexport type Runner = (\n cmd: string,\n args: string[],\n cwd: string,\n env?: NodeJS.ProcessEnv,\n) => Promise<void>\n\n/** The port `renderConfig` gives the API, which serves the admin at /admin/. */\nconst API_PORT = 3001\n\ninterface AdminCredentials {\n email: string\n password: string\n}\n\n/** A fresh admin password per project (128 bits, base64url), so no two stores share the seed's default. */\nfunction newAdminCredentials(): AdminCredentials {\n return { email: 'admin@nymbal.local', password: randomBytes(16).toString('base64url') }\n}\n\n/** `.env` holds the admin credentials the seed (and a first `nymbal dev`) creates the admin from. */\nasync function writeAdminEnv(dir: string, admin: AdminCredentials): Promise<void> {\n const lines = [\n '# The admin account the seed creates. Keep this file out of version control (.gitignore lists it).',\n `NYMBAL_ADMIN_EMAIL=${admin.email}`,\n `NYMBAL_ADMIN_PASSWORD=${admin.password}`,\n '',\n ]\n await writeFile(resolve(dir, '.env'), lines.join('\\n'), { encoding: 'utf8', mode: 0o600 })\n}\n\nfunction adminSignIn(admin: AdminCredentials): string[] {\n return [\n `Admin: http://localhost:${API_PORT}/admin/`,\n ` email ${admin.email}`,\n ` password ${admin.password} (saved in .env)`,\n ]\n}\n\nexport interface MainDeps {\n run?: Runner\n}\n\n/**\n * The `packageManager` field for the package manager that launched us (`npm_config_user_agent`), read when called.\n * Without a usable user agent it is the installed npm; undefined when npm cannot be run either.\n */\nfunction packageManagerField(): string | undefined {\n const token = (process.env['npm_config_user_agent'] ?? '').split(' ')[0] ?? ''\n const match = /^(npm|pnpm|yarn|bun)\\/(\\d+\\.\\d+\\.\\d+\\S*)$/.exec(token)\n if (match) return `${match[1]}@${match[2]}`\n const npm = spawnSync('npm', ['-v'], { encoding: 'utf8', shell: process.platform === 'win32' })\n const version = npm.status === 0 ? npm.stdout.trim() : ''\n return version ? `npm@${version}` : undefined\n}\n\nexport function detectPackageManager(): PackageManager {\n const ua = process.env['npm_config_user_agent']\n if (!ua) return 'npm'\n const token = ua.split(' ')[0] ?? ''\n const slash = token.indexOf('/')\n const pm = slash >= 0 ? token.slice(0, slash) : token\n const version = slash >= 0 ? token.slice(slash + 1) : ''\n if (pm === 'pnpm') return 'pnpm'\n if (pm === 'bun') return 'bun'\n if (pm === 'yarn') {\n const major = parseInt(version.split('.')[0] ?? '1', 10)\n return major < 2 ? 'yarn-classic' : 'yarn-berry'\n }\n return 'npm'\n}\n\nfunction installCmd(pm: PackageManager): [string, string[]] {\n switch (pm) {\n case 'pnpm':\n return ['pnpm', ['install']]\n case 'bun':\n return ['bun', ['install']]\n case 'yarn-classic':\n case 'yarn-berry':\n return ['yarn', ['install']]\n default:\n return ['npm', ['install']]\n }\n}\n\nfunction seedCmd(pm: PackageManager): [string, string[]] {\n switch (pm) {\n case 'pnpm':\n return ['pnpm', ['exec', 'nymbal', 'seed']]\n case 'bun':\n return ['bunx', ['nymbal', 'seed']]\n case 'yarn-classic':\n return ['yarn', ['run', 'nymbal', 'seed']]\n case 'yarn-berry':\n return ['yarn', ['exec', 'nymbal', 'seed']]\n default:\n return ['npx', ['nymbal', 'seed']]\n }\n}\n\nfunction devCmd(pm: PackageManager): string {\n switch (pm) {\n case 'pnpm':\n return 'pnpm dev'\n case 'bun':\n return 'bun dev'\n case 'yarn-classic':\n case 'yarn-berry':\n return 'yarn dev'\n default:\n return 'npm run dev'\n }\n}\n\nconst TEMPLATES = ['astro', 'nextjs'] as const\nconst CURRENCIES = ['GBP', 'USD', 'EUR'] as const\n\nexport interface CliFlags {\n name?: string\n template?: (typeof TEMPLATES)[number]\n currency?: (typeof CURRENCIES)[number]\n yes: boolean\n /** Present (false) only with `--no-install`. */\n install?: boolean\n}\n\n/** A command-line flag that is missing its value or names a value outside the allowed set. */\nexport class CliFlagError extends Error {\n constructor(message: string) {\n super(message)\n this.name = 'CliFlagError'\n }\n}\n\nfunction oneOf<T extends string>(flag: string, value: string, allowed: readonly T[]): T {\n if ((allowed as readonly string[]).includes(value)) return value as T\n throw new CliFlagError(`Unknown ${flag} \"${value}\". Allowed: ${allowed.join(', ')}`)\n}\n\n/** Parses process.argv (node and script first): --name, --template and --currency take `--flag v` or `--flag=v`. */\nexport function parseCliFlags(argv: string[]): CliFlags {\n const flags: CliFlags = { yes: false }\n const args = argv.slice(2)\n let positional: string | undefined\n for (let i = 0; i < args.length; i++) {\n const arg = args[i]!\n if (arg === '--yes' || arg === '-y') {\n flags.yes = true\n continue\n }\n if (arg === '--no-install') {\n flags.install = false\n continue\n }\n const match = /^--(name|template|currency)(?:=(.*))?$/s.exec(arg)\n if (match) {\n const flag = match[1]!\n let value = match[2]\n if (value === undefined) {\n const next = args[i + 1]\n if (next === undefined || next.startsWith('-'))\n throw new CliFlagError(`--${flag} needs a value`)\n value = next\n i++\n }\n if (flag === 'name') flags.name = value\n else if (flag === 'template') flags.template = oneOf('--template', value, TEMPLATES)\n else flags.currency = oneOf('--currency', value, CURRENCIES)\n } else if (!arg.startsWith('-') && positional === undefined) {\n positional = arg\n }\n }\n if (flags.name === undefined && positional !== undefined) flags.name = positional\n return flags\n}\n\nexport async function main(argv: string[] = process.argv, deps: MainDeps = {}): Promise<void> {\n const run = deps.run ?? runCmd\n let flags: CliFlags\n try {\n flags = parseCliFlags(argv)\n } catch (err) {\n if (!(err instanceof CliFlagError)) throw err\n process.stderr.write(`Error: ${err.message}\\n`)\n process.exit(1)\n }\n\n const pm = detectPackageManager()\n\n if (flags.yes) {\n if (!flags.name) {\n process.stderr.write('Error: --yes requires --name or a positional name argument\\n')\n process.exit(1)\n }\n const name = flags.name.trim()\n const answers: Answers = {\n name,\n directory: toDirectoryName(name),\n currency: flags.currency ?? 'GBP',\n template: flags.template ?? 'astro',\n seedDemo: true,\n }\n const targetDir = resolve(process.cwd(), answers.directory)\n if (existsSync(targetDir)) {\n const entries = await readdir(targetDir)\n if (entries.length > 0) {\n process.stderr.write(\n `Error: Directory ${answers.directory} already exists and is not empty.\\n`,\n )\n process.exit(1)\n }\n } else {\n await mkdir(targetDir, { recursive: true })\n }\n await scaffold(targetDir, answers)\n const admin = newAdminCredentials()\n await writeAdminEnv(targetDir, admin)\n process.stdout.write(`Created ${answers.directory}\\n`)\n if (flags.install !== false) {\n await installAndSeed(targetDir, pm, answers.seedDemo, run, admin)\n } else {\n process.stdout.write(`Next: cd ${answers.directory} && ${installCmd(pm)[0]} install\\n`)\n }\n process.stdout.write(`${adminSignIn(admin).join('\\n')}\\n`)\n return\n }\n\n intro(pc.cyan(pc.bold('◆ create-nymbal-app')))\n\n const name = flags.name\n ? flags.name\n : ((await text({\n message: 'Store name',\n placeholder: 'My Store',\n validate: (v) => (!v || v.trim().length === 0 ? 'Required' : undefined),\n })) as string | symbol)\n\n if (isCancel(name)) return cancel('Cancelled')\n\n type Currency = 'GBP' | 'USD' | 'EUR'\n const currencyOptions: { value: Currency; label: string }[] = [\n { value: 'GBP', label: 'GBP — British Pound' },\n { value: 'USD', label: 'USD — US Dollar' },\n { value: 'EUR', label: 'EUR — Euro' },\n ]\n const currency = await select({\n message: 'Store currency',\n options: currencyOptions,\n initialValue: 'GBP' as Currency,\n })\n\n if (isCancel(currency)) return cancel('Cancelled')\n\n type Template = 'astro' | 'nextjs'\n const templateOptions: { value: Template; label: string }[] = [\n { value: 'astro', label: 'Astro (recommended — performance-first)' },\n { value: 'nextjs', label: 'Next.js (migration-friendly)' },\n ]\n const template = await select({\n message: 'Storefront template',\n options: templateOptions,\n initialValue: 'astro' as Template,\n })\n\n if (isCancel(template)) return cancel('Cancelled')\n\n const seedDemo = (await confirm({\n message: 'Generate demo content?',\n initialValue: true,\n })) as boolean | symbol\n\n if (isCancel(seedDemo)) return cancel('Cancelled')\n\n const shouldInstall =\n flags.install === false\n ? false\n : ((await confirm({\n message: 'Install dependencies now?',\n initialValue: true,\n })) as boolean | symbol)\n\n if (isCancel(shouldInstall)) return cancel('Cancelled')\n\n const answers: Answers = {\n name: String(name).trim(),\n directory: toDirectoryName(String(name).trim()),\n currency: currency as 'GBP' | 'USD' | 'EUR',\n template: template as 'astro' | 'nextjs',\n seedDemo: seedDemo as boolean,\n }\n\n const targetDir = resolve(process.cwd(), answers.directory)\n if (existsSync(targetDir)) {\n const entries = await readdir(targetDir)\n if (entries.length > 0) {\n cancel(`Directory ${answers.directory} already exists and is not empty.`)\n process.exit(1)\n }\n } else {\n await mkdir(targetDir, { recursive: true })\n }\n\n note(pc.dim(`Scaffolding into ${targetDir}`), 'Creating project')\n\n await scaffold(targetDir, answers)\n const admin = newAdminCredentials()\n await writeAdminEnv(targetDir, admin)\n\n if (shouldInstall) {\n await installAndSeed(targetDir, pm, answers.seedDemo, run, admin)\n }\n\n const cmd = devCmd(pm)\n const nextSteps = [` ${pc.cyan('cd')} ${answers.directory}`]\n if (!shouldInstall) {\n const [ic] = installCmd(pm)\n nextSteps.push(` ${pc.cyan(ic + ' install')}`)\n }\n nextSteps.push(` ${pc.cyan(cmd)}`)\n\n outro(\n [pc.green(`✓ Created ${answers.directory}`), '', ...nextSteps, '', ...adminSignIn(admin)].join(\n '\\n',\n ),\n )\n}\n\n/** Install with the detected package manager, then seed through the project's own `nymbal` bin. */\nasync function installAndSeed(\n dir: string,\n pm: PackageManager,\n seedDemo: boolean,\n run: Runner,\n admin: AdminCredentials,\n): Promise<void> {\n const [installBin, installArgs] = installCmd(pm)\n process.stdout.write(`Running ${installBin} install (this may take a moment)...\\n`)\n await run(installBin, installArgs, dir)\n if (seedDemo) {\n process.stdout.write('Seeding demo content...\\n')\n const [seedBin, seedArgs] = seedCmd(pm)\n // Explicit, so a value exported in the shell cannot replace the one in .env and the outro.\n await run(seedBin, seedArgs, dir, {\n ...process.env,\n NYMBAL_ADMIN_EMAIL: admin.email,\n NYMBAL_ADMIN_PASSWORD: admin.password,\n })\n }\n}\n\nexport async function scaffold(dir: string, answers: Answers): Promise<void> {\n // Copy the chosen template directly into dir (flat — no storefront/ subdirectory)\n await copyScaffoldDir(resolve(SCAFFOLD_ROOT, 'templates', answers.template), dir)\n\n // Overlay the project layer (scaffold/project/<template>) on top of the template\n // defaults. Project files WIN on path collision — this is the override model:\n // anything in the project layer shadows the template default at the same path.\n // scaffold/project/ lives outside scaffold/templates/, so sync-scaffold's\n // rm -rf of scaffold/templates/* never deletes it.\n const projectLayer = resolve(SCAFFOLD_ROOT, 'project', answers.template)\n if (existsSync(projectLayer)) {\n await copyScaffoldDir(projectLayer, dir)\n }\n\n // Write nymbal.config.ts (answers-dependent, so generated at runtime)\n await writeFile(resolve(dir, 'nymbal.config.ts'), renderConfig(answers), 'utf8')\n\n // Patch the copied package.json: set name to the project directory name\n await patchPackageJson(dir, answers.directory)\n}\n\nasync function patchPackageJson(dir: string, name: string): Promise<void> {\n const pkgPath = resolve(dir, 'package.json')\n const pkg = JSON.parse(await readFile(pkgPath, 'utf8')) as Record<string, unknown>\n pkg['name'] = name\n pkg['private'] = true\n const packageManager = packageManagerField()\n if (packageManager) pkg['packageManager'] = packageManager\n await writeFile(pkgPath, JSON.stringify(pkg, null, 2) + '\\n', 'utf8')\n}\n\nasync function copyScaffoldDir(src: string, dest: string): Promise<void> {\n if (!existsSync(src)) {\n throw new Error(`Scaffold directory not found: ${src}`)\n }\n await cp(src, dest, {\n recursive: true,\n filter: (source) => !source.endsWith('node_modules'),\n })\n await renameDotFiles(dest)\n}\n\nasync function renameDotFiles(dir: string): Promise<void> {\n // Non-recursive: only rename _dot.* files at the project root.\n // All shared dotfiles (_dot.gitignore, _dot.env.example) live at template root.\n const entries = await readdir(dir, { withFileTypes: true })\n for (const entry of entries) {\n if (!entry.isDirectory() && entry.name.startsWith('_dot.')) {\n const full = resolve(dir, entry.name)\n const renamed = resolve(dir, '.' + entry.name.slice(5))\n await writeFile(renamed, await readFile(full))\n await rm(full)\n }\n }\n}\n\nfunction renderConfig(answers: Answers): string {\n return `import { defineConfig } from '@nymbalhq/config'\n\nexport default defineConfig({\n store: {\n name: '${escape(answers.name)}',\n currency: '${answers.currency}',\n locale: '${defaultLocale(answers.currency)}',\n timezone: '${defaultTimezone(answers.currency)}',\n },\n\n template: '${answers.template}',\n\n infrastructure: {\n commandStore: 'sqlite',\n documentStore: 'in-memory',\n eventBus: 'in-process',\n },\n\n security: {\n adapter: 'middleware',\n rateLimit: {\n '/checkout': { requests: 10, window: '60s', action: 'block' },\n '/auth/*': { requests: 5, window: '60s', action: 'challenge' },\n '/api/*': { requests: 100, window: '60s', action: 'throttle' },\n '/webhooks/*': { requests: 200, window: '60s', action: 'throttle' },\n },\n botProtection: { mode: 'managed', allowList: ['googlebot', 'bingbot', 'stripe-webhooks'] },\n headers: {\n hsts: true,\n contentSecurityPolicy: 'strict',\n referrerPolicy: 'strict-origin-when-cross-origin',\n xFrameOptions: 'deny',\n },\n csrf: true,\n },\n\n commerce: {\n payments: { provider: 'native' },\n email: { provider: 'native' },\n reviews: { provider: 'native' },\n search: { provider: 'native' },\n analytics: { provider: 'native' },\n shipping: { provider: 'native' },\n tax: { provider: 'native' },\n ai: { provider: 'none' },\n },\n\n http: {\n adapter: 'fastify',\n port: ${API_PORT},\n },\n\n deployment: {\n strategy: 'standard',\n },\n\n features: {\n staging: false,\n heartbeat: false,\n autoUpdates: false,\n },\n})\n`\n}\n\nfunction defaultLocale(currency: Answers['currency']): string {\n return currency === 'USD' ? 'en-US' : 'en-GB'\n}\n\nfunction defaultTimezone(currency: Answers['currency']): string {\n return currency === 'USD' ? 'America/New_York' : 'Europe/London'\n}\n\nfunction escape(v: string): string {\n return v.replace(/'/g, \"\\\\'\")\n}\n\nfunction toDirectoryName(name: string): string {\n return name\n .toLowerCase()\n .replace(/[^a-z0-9-_ ]/g, '')\n .trim()\n .replace(/\\s+/g, '-')\n}\n\nconst runCmd: Runner = (cmd, args, cwd, env = process.env) => {\n return new Promise((resolve, reject) => {\n const child = spawn(cmd, args, { cwd, env, stdio: 'inherit', shell: false })\n child.on('error', reject)\n child.on('exit', (code) => {\n if (code === 0) return resolve()\n reject(new Error(`${cmd} ${args.join(' ')} exited with ${code}`))\n })\n })\n}\n"],"mappings":"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","names":["name","answers","targetDir","admin","resolve"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["import { cp, readFile, mkdir, readdir, rm, writeFile } from 'node:fs/promises'\nimport { existsSync } from 'node:fs'\nimport { dirname, resolve } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport { spawn, spawnSync } from 'node:child_process'\nimport { randomBytes } from 'node:crypto'\nimport { cancel, confirm, intro, isCancel, note, outro, select, text } from '@clack/prompts'\nimport pc from 'picocolors'\n\nconst HERE = dirname(fileURLToPath(import.meta.url))\nconst SCAFFOLD_ROOT = resolve(HERE, '..', 'scaffold')\n\nexport type PackageManager = 'npm' | 'pnpm' | 'yarn-classic' | 'yarn-berry' | 'bun'\n\ninterface Answers {\n name: string\n directory: string\n currency: 'GBP' | 'USD' | 'EUR'\n template: 'astro' | 'nextjs'\n seedDemo: boolean\n}\n\n/**\n * Runs a command in `cwd` (with `env` when given, else the process environment) and rejects on a non-zero exit.\n * Injected into `main` so tests do not shell out.\n */\nexport type Runner = (\n cmd: string,\n args: string[],\n cwd: string,\n env?: NodeJS.ProcessEnv,\n) => Promise<void>\n\n/** The port `renderConfig` gives the API, which serves the admin at /admin/. */\nconst API_PORT = 3001\n\ninterface AdminCredentials {\n email: string\n password: string\n}\n\n/** A fresh admin password per project (128 bits, base64url), so no two stores share the seed's default. */\nfunction newAdminCredentials(): AdminCredentials {\n return { email: 'admin@nymbal.local', password: randomBytes(16).toString('base64url') }\n}\n\n/**\n * `.env` holds the admin credentials the seed (and a first `nymbal dev`) creates the admin from, and the project's own\n * key for the values the admin's Settings encrypt (32 random bytes, base64).\n */\nasync function writeAdminEnv(dir: string, admin: AdminCredentials): Promise<void> {\n const lines = [\n '# The admin account the seed creates. Keep this file out of version control (.gitignore lists it).',\n `NYMBAL_ADMIN_EMAIL=${admin.email}`,\n `NYMBAL_ADMIN_PASSWORD=${admin.password}`,\n '',\n \"# Encrypts the keys saved in the admin's Settings. Losing it loses them: back it up with your deploy's secrets.\",\n `NYMBAL_SECRETS_KEY=${randomBytes(32).toString('base64')}`,\n '',\n ]\n await writeFile(resolve(dir, '.env'), lines.join('\\n'), { encoding: 'utf8', mode: 0o600 })\n}\n\nfunction adminSignIn(admin: AdminCredentials): string[] {\n return [\n `Admin: http://localhost:${API_PORT}/admin/`,\n ` email ${admin.email}`,\n ` password ${admin.password} (saved in .env)`,\n ]\n}\n\nexport interface MainDeps {\n run?: Runner\n}\n\n/**\n * The `packageManager` field for the package manager that launched us (`npm_config_user_agent`), read when called.\n * Without a usable user agent it is the installed npm; undefined when npm cannot be run either.\n */\nfunction packageManagerField(): string | undefined {\n const token = (process.env['npm_config_user_agent'] ?? '').split(' ')[0] ?? ''\n const match = /^(npm|pnpm|yarn|bun)\\/(\\d+\\.\\d+\\.\\d+\\S*)$/.exec(token)\n if (match) return `${match[1]}@${match[2]}`\n const npm = spawnSync('npm', ['-v'], { encoding: 'utf8', shell: process.platform === 'win32' })\n const version = npm.status === 0 ? npm.stdout.trim() : ''\n return version ? `npm@${version}` : undefined\n}\n\nexport function detectPackageManager(): PackageManager {\n const ua = process.env['npm_config_user_agent']\n if (!ua) return 'npm'\n const token = ua.split(' ')[0] ?? ''\n const slash = token.indexOf('/')\n const pm = slash >= 0 ? token.slice(0, slash) : token\n const version = slash >= 0 ? token.slice(slash + 1) : ''\n if (pm === 'pnpm') return 'pnpm'\n if (pm === 'bun') return 'bun'\n if (pm === 'yarn') {\n const major = parseInt(version.split('.')[0] ?? '1', 10)\n return major < 2 ? 'yarn-classic' : 'yarn-berry'\n }\n return 'npm'\n}\n\nfunction installCmd(pm: PackageManager): [string, string[]] {\n switch (pm) {\n case 'pnpm':\n return ['pnpm', ['install']]\n case 'bun':\n return ['bun', ['install']]\n case 'yarn-classic':\n case 'yarn-berry':\n return ['yarn', ['install']]\n default:\n return ['npm', ['install']]\n }\n}\n\nfunction seedCmd(pm: PackageManager): [string, string[]] {\n switch (pm) {\n case 'pnpm':\n return ['pnpm', ['exec', 'nymbal', 'seed']]\n case 'bun':\n return ['bunx', ['nymbal', 'seed']]\n case 'yarn-classic':\n return ['yarn', ['run', 'nymbal', 'seed']]\n case 'yarn-berry':\n return ['yarn', ['exec', 'nymbal', 'seed']]\n default:\n return ['npx', ['nymbal', 'seed']]\n }\n}\n\nfunction devCmd(pm: PackageManager): string {\n switch (pm) {\n case 'pnpm':\n return 'pnpm dev'\n case 'bun':\n return 'bun dev'\n case 'yarn-classic':\n case 'yarn-berry':\n return 'yarn dev'\n default:\n return 'npm run dev'\n }\n}\n\nconst TEMPLATES = ['astro', 'nextjs'] as const\nconst CURRENCIES = ['GBP', 'USD', 'EUR'] as const\n\nexport interface CliFlags {\n name?: string\n template?: (typeof TEMPLATES)[number]\n currency?: (typeof CURRENCIES)[number]\n yes: boolean\n /** Present (false) only with `--no-install`. */\n install?: boolean\n}\n\n/** A command-line flag that is missing its value or names a value outside the allowed set. */\nexport class CliFlagError extends Error {\n constructor(message: string) {\n super(message)\n this.name = 'CliFlagError'\n }\n}\n\nfunction oneOf<T extends string>(flag: string, value: string, allowed: readonly T[]): T {\n if ((allowed as readonly string[]).includes(value)) return value as T\n throw new CliFlagError(`Unknown ${flag} \"${value}\". Allowed: ${allowed.join(', ')}`)\n}\n\n/** Parses process.argv (node and script first): --name, --template and --currency take `--flag v` or `--flag=v`. */\nexport function parseCliFlags(argv: string[]): CliFlags {\n const flags: CliFlags = { yes: false }\n const args = argv.slice(2)\n let positional: string | undefined\n for (let i = 0; i < args.length; i++) {\n const arg = args[i]!\n if (arg === '--yes' || arg === '-y') {\n flags.yes = true\n continue\n }\n if (arg === '--no-install') {\n flags.install = false\n continue\n }\n const match = /^--(name|template|currency)(?:=(.*))?$/s.exec(arg)\n if (match) {\n const flag = match[1]!\n let value = match[2]\n if (value === undefined) {\n const next = args[i + 1]\n if (next === undefined || next.startsWith('-'))\n throw new CliFlagError(`--${flag} needs a value`)\n value = next\n i++\n }\n if (flag === 'name') flags.name = value\n else if (flag === 'template') flags.template = oneOf('--template', value, TEMPLATES)\n else flags.currency = oneOf('--currency', value, CURRENCIES)\n } else if (!arg.startsWith('-') && positional === undefined) {\n positional = arg\n }\n }\n if (flags.name === undefined && positional !== undefined) flags.name = positional\n return flags\n}\n\nexport async function main(argv: string[] = process.argv, deps: MainDeps = {}): Promise<void> {\n const run = deps.run ?? runCmd\n let flags: CliFlags\n try {\n flags = parseCliFlags(argv)\n } catch (err) {\n if (!(err instanceof CliFlagError)) throw err\n process.stderr.write(`Error: ${err.message}\\n`)\n process.exit(1)\n }\n\n const pm = detectPackageManager()\n\n if (flags.yes) {\n if (!flags.name) {\n process.stderr.write('Error: --yes requires --name or a positional name argument\\n')\n process.exit(1)\n }\n const name = flags.name.trim()\n const answers: Answers = {\n name,\n directory: toDirectoryName(name),\n currency: flags.currency ?? 'GBP',\n template: flags.template ?? 'astro',\n seedDemo: true,\n }\n const targetDir = resolve(process.cwd(), answers.directory)\n if (existsSync(targetDir)) {\n const entries = await readdir(targetDir)\n if (entries.length > 0) {\n process.stderr.write(\n `Error: Directory ${answers.directory} already exists and is not empty.\\n`,\n )\n process.exit(1)\n }\n } else {\n await mkdir(targetDir, { recursive: true })\n }\n await scaffold(targetDir, answers)\n const admin = newAdminCredentials()\n await writeAdminEnv(targetDir, admin)\n process.stdout.write(`Created ${answers.directory}\\n`)\n if (flags.install !== false) {\n await installAndSeed(targetDir, pm, answers.seedDemo, run, admin)\n } else {\n process.stdout.write(`Next: cd ${answers.directory} && ${installCmd(pm)[0]} install\\n`)\n }\n process.stdout.write(`${adminSignIn(admin).join('\\n')}\\n`)\n return\n }\n\n intro(pc.cyan(pc.bold('◆ create-nymbal-app')))\n\n const name = flags.name\n ? flags.name\n : ((await text({\n message: 'Store name',\n placeholder: 'My Store',\n validate: (v) => (!v || v.trim().length === 0 ? 'Required' : undefined),\n })) as string | symbol)\n\n if (isCancel(name)) return cancel('Cancelled')\n\n type Currency = 'GBP' | 'USD' | 'EUR'\n const currencyOptions: { value: Currency; label: string }[] = [\n { value: 'GBP', label: 'GBP — British Pound' },\n { value: 'USD', label: 'USD — US Dollar' },\n { value: 'EUR', label: 'EUR — Euro' },\n ]\n const currency = await select({\n message: 'Store currency',\n options: currencyOptions,\n initialValue: 'GBP' as Currency,\n })\n\n if (isCancel(currency)) return cancel('Cancelled')\n\n type Template = 'astro' | 'nextjs'\n const templateOptions: { value: Template; label: string }[] = [\n { value: 'astro', label: 'Astro (recommended — performance-first)' },\n { value: 'nextjs', label: 'Next.js (migration-friendly)' },\n ]\n const template = await select({\n message: 'Storefront template',\n options: templateOptions,\n initialValue: 'astro' as Template,\n })\n\n if (isCancel(template)) return cancel('Cancelled')\n\n const seedDemo = (await confirm({\n message: 'Generate demo content?',\n initialValue: true,\n })) as boolean | symbol\n\n if (isCancel(seedDemo)) return cancel('Cancelled')\n\n const shouldInstall =\n flags.install === false\n ? false\n : ((await confirm({\n message: 'Install dependencies now?',\n initialValue: true,\n })) as boolean | symbol)\n\n if (isCancel(shouldInstall)) return cancel('Cancelled')\n\n const answers: Answers = {\n name: String(name).trim(),\n directory: toDirectoryName(String(name).trim()),\n currency: currency as 'GBP' | 'USD' | 'EUR',\n template: template as 'astro' | 'nextjs',\n seedDemo: seedDemo as boolean,\n }\n\n const targetDir = resolve(process.cwd(), answers.directory)\n if (existsSync(targetDir)) {\n const entries = await readdir(targetDir)\n if (entries.length > 0) {\n cancel(`Directory ${answers.directory} already exists and is not empty.`)\n process.exit(1)\n }\n } else {\n await mkdir(targetDir, { recursive: true })\n }\n\n note(pc.dim(`Scaffolding into ${targetDir}`), 'Creating project')\n\n await scaffold(targetDir, answers)\n const admin = newAdminCredentials()\n await writeAdminEnv(targetDir, admin)\n\n if (shouldInstall) {\n await installAndSeed(targetDir, pm, answers.seedDemo, run, admin)\n }\n\n const cmd = devCmd(pm)\n const nextSteps = [` ${pc.cyan('cd')} ${answers.directory}`]\n if (!shouldInstall) {\n const [ic] = installCmd(pm)\n nextSteps.push(` ${pc.cyan(ic + ' install')}`)\n }\n nextSteps.push(` ${pc.cyan(cmd)}`)\n\n outro(\n [pc.green(`✓ Created ${answers.directory}`), '', ...nextSteps, '', ...adminSignIn(admin)].join(\n '\\n',\n ),\n )\n}\n\n/** Install with the detected package manager, then seed through the project's own `nymbal` bin. */\nasync function installAndSeed(\n dir: string,\n pm: PackageManager,\n seedDemo: boolean,\n run: Runner,\n admin: AdminCredentials,\n): Promise<void> {\n const [installBin, installArgs] = installCmd(pm)\n process.stdout.write(`Running ${installBin} install (this may take a moment)...\\n`)\n await run(installBin, installArgs, dir)\n if (seedDemo) {\n process.stdout.write('Seeding demo content...\\n')\n const [seedBin, seedArgs] = seedCmd(pm)\n // Explicit, so a value exported in the shell cannot replace the one in .env and the outro.\n await run(seedBin, seedArgs, dir, {\n ...process.env,\n NYMBAL_ADMIN_EMAIL: admin.email,\n NYMBAL_ADMIN_PASSWORD: admin.password,\n })\n }\n}\n\nexport async function scaffold(dir: string, answers: Answers): Promise<void> {\n // Copy the chosen template directly into dir (flat — no storefront/ subdirectory)\n await copyScaffoldDir(resolve(SCAFFOLD_ROOT, 'templates', answers.template), dir)\n\n // Overlay the project layer (scaffold/project/<template>) on top of the template\n // defaults. Project files WIN on path collision — this is the override model:\n // anything in the project layer shadows the template default at the same path.\n // scaffold/project/ lives outside scaffold/templates/, so sync-scaffold's\n // rm -rf of scaffold/templates/* never deletes it.\n const projectLayer = resolve(SCAFFOLD_ROOT, 'project', answers.template)\n if (existsSync(projectLayer)) {\n await copyScaffoldDir(projectLayer, dir)\n }\n\n // Write nymbal.config.ts (answers-dependent, so generated at runtime)\n await writeFile(resolve(dir, 'nymbal.config.ts'), renderConfig(answers), 'utf8')\n\n // Patch the copied package.json: set name to the project directory name\n await patchPackageJson(dir, answers.directory)\n}\n\nasync function patchPackageJson(dir: string, name: string): Promise<void> {\n const pkgPath = resolve(dir, 'package.json')\n const pkg = JSON.parse(await readFile(pkgPath, 'utf8')) as Record<string, unknown>\n pkg['name'] = name\n pkg['private'] = true\n const packageManager = packageManagerField()\n if (packageManager) pkg['packageManager'] = packageManager\n await writeFile(pkgPath, JSON.stringify(pkg, null, 2) + '\\n', 'utf8')\n}\n\nasync function copyScaffoldDir(src: string, dest: string): Promise<void> {\n if (!existsSync(src)) {\n throw new Error(`Scaffold directory not found: ${src}`)\n }\n await cp(src, dest, {\n recursive: true,\n filter: (source) => !source.endsWith('node_modules'),\n })\n await renameDotFiles(dest)\n}\n\nasync function renameDotFiles(dir: string): Promise<void> {\n // Non-recursive: only rename _dot.* files at the project root.\n // All shared dotfiles (_dot.gitignore, _dot.env.example) live at template root.\n const entries = await readdir(dir, { withFileTypes: true })\n for (const entry of entries) {\n if (!entry.isDirectory() && entry.name.startsWith('_dot.')) {\n const full = resolve(dir, entry.name)\n const renamed = resolve(dir, '.' + entry.name.slice(5))\n await writeFile(renamed, await readFile(full))\n await rm(full)\n }\n }\n}\n\nfunction renderConfig(answers: Answers): string {\n return `import { defineConfig } from '@nymbalhq/config'\n\nexport default defineConfig({\n store: {\n name: '${escape(answers.name)}',\n currency: '${answers.currency}',\n locale: '${defaultLocale(answers.currency)}',\n timezone: '${defaultTimezone(answers.currency)}',\n },\n\n template: '${answers.template}',\n\n infrastructure: {\n commandStore: 'sqlite',\n documentStore: 'in-memory',\n eventBus: 'in-process',\n },\n\n security: {\n adapter: 'middleware',\n rateLimit: {\n '/checkout': { requests: 10, window: '1m', action: 'block' },\n '/auth/*': { requests: 5, window: '1m', action: 'challenge' },\n '/api/*': { requests: 100, window: '1m', action: 'throttle' },\n '/webhooks/*': { requests: 200, window: '1m', action: 'throttle' },\n },\n botProtection: { mode: 'managed', allowList: ['googlebot', 'bingbot', 'stripe-webhooks'] },\n headers: {\n hsts: true,\n contentSecurityPolicy: 'strict',\n referrerPolicy: 'strict-origin-when-cross-origin',\n xFrameOptions: 'deny',\n },\n csrf: true,\n },\n\n commerce: {\n payments: { provider: 'native' },\n email: { provider: 'native' },\n reviews: { provider: 'native' },\n search: { provider: 'native' },\n analytics: { provider: 'native' },\n shipping: { provider: 'native' },\n tax: { provider: 'native' },\n ai: { provider: 'none' },\n },\n\n http: {\n adapter: 'fastify',\n port: ${API_PORT},\n },\n\n deployment: {\n strategy: 'standard',\n },\n\n features: {\n staging: false,\n heartbeat: false,\n autoUpdates: false,\n },\n})\n`\n}\n\nfunction defaultLocale(currency: Answers['currency']): string {\n return currency === 'USD' ? 'en-US' : 'en-GB'\n}\n\nfunction defaultTimezone(currency: Answers['currency']): string {\n return currency === 'USD' ? 'America/New_York' : 'Europe/London'\n}\n\nfunction escape(v: string): string {\n return v.replace(/'/g, \"\\\\'\")\n}\n\nfunction toDirectoryName(name: string): string {\n return name\n .toLowerCase()\n .replace(/[^a-z0-9-_ ]/g, '')\n .trim()\n .replace(/\\s+/g, '-')\n}\n\nconst runCmd: Runner = (cmd, args, cwd, env = process.env) => {\n return new Promise((resolve, reject) => {\n const child = spawn(cmd, args, { cwd, env, stdio: 'inherit', shell: false })\n child.on('error', reject)\n child.on('exit', (code) => {\n if (code === 0) return resolve()\n reject(new Error(`${cmd} ${args.join(' ')} exited with ${code}`))\n })\n })\n}\n"],"mappings":"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","names":["name","answers","targetDir","admin","resolve"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-nymbal-app",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Scaffold a new Nymbal commerce project",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -30,8 +30,9 @@
|
|
|
30
30
|
"picocolors": "^1.1.1"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
|
-
"
|
|
34
|
-
"
|
|
33
|
+
"@typescript/native": "npm:typescript@^7.0.2",
|
|
34
|
+
"tsup": "^9.0.0",
|
|
35
|
+
"typescript": "npm:@typescript/typescript6@^6.0.2",
|
|
35
36
|
"vitest": "^3.0.0"
|
|
36
37
|
},
|
|
37
38
|
"scripts": {
|
|
@@ -39,6 +40,6 @@
|
|
|
39
40
|
"dev": "tsup --watch",
|
|
40
41
|
"sync:scaffold": "node ./scripts/sync-scaffold.mjs",
|
|
41
42
|
"test": "vitest run",
|
|
42
|
-
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
43
|
+
"typecheck": "tsc -p tsconfig.json --noEmit --rootDir ../.."
|
|
43
44
|
}
|
|
44
45
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
---
|
|
2
2
|
import '../styles/pages/home.css'
|
|
3
|
+
import NymbalElement from '@nymbalhq/web-components/astro'
|
|
4
|
+
import { cardProductOf } from '@nymbalhq/web-components/ssr'
|
|
3
5
|
import BaseLayout from '../layouts/BaseLayout.astro'
|
|
4
6
|
import HeroSection from '../components/HeroSection.astro'
|
|
5
|
-
import CategoryNav from '../components/CategoryNav.astro'
|
|
6
|
-
import ProductCard from '../components/ProductCard.astro'
|
|
7
7
|
import NewsletterSignup from '../components/NewsletterSignup.astro'
|
|
8
8
|
import { fetchProducts, fetchCategories } from '../lib/api'
|
|
9
9
|
import { organizationJsonLd } from '../lib/seo'
|
|
@@ -13,7 +13,6 @@ const [productResult, categories] = await Promise.all([
|
|
|
13
13
|
fetchCategories(),
|
|
14
14
|
])
|
|
15
15
|
|
|
16
|
-
const products = productResult.items
|
|
17
16
|
const jsonLd = organizationJsonLd('Nymbal', Astro.url.origin)
|
|
18
17
|
---
|
|
19
18
|
|
|
@@ -30,7 +29,7 @@ const jsonLd = organizationJsonLd('Nymbal', Astro.url.origin)
|
|
|
30
29
|
Welcome to your store — this homepage is yours to customise.
|
|
31
30
|
</p>
|
|
32
31
|
<h2 class="section-title">Shop by Category</h2>
|
|
33
|
-
<
|
|
32
|
+
<NymbalElement tag="nymbal-category-nav" attributes={{ categories: JSON.stringify(categories) }} />
|
|
34
33
|
</section>
|
|
35
34
|
|
|
36
35
|
<section class="featured-section section">
|
|
@@ -39,19 +38,18 @@ const jsonLd = organizationJsonLd('Nymbal', Astro.url.origin)
|
|
|
39
38
|
<h2 class="section-title" style="margin-bottom: 0;">Our Featured Picks</h2>
|
|
40
39
|
<a href="/products" class="featured-link">View all</a>
|
|
41
40
|
</div>
|
|
42
|
-
{products.
|
|
43
|
-
<div class="product-grid">
|
|
44
|
-
{products.map((product) => (
|
|
45
|
-
<ProductCard product={product} />
|
|
46
|
-
))}
|
|
47
|
-
</div>
|
|
48
|
-
) : (
|
|
49
|
-
<p style="text-align: center; color: var(--nymbal-color-text-muted, #71717a);">
|
|
50
|
-
Products coming soon.
|
|
51
|
-
</p>
|
|
52
|
-
)}
|
|
41
|
+
<NymbalElement tag="nymbal-product-grid" attributes={{ products: JSON.stringify(productResult.items.map(cardProductOf)) }} />
|
|
53
42
|
</div>
|
|
54
43
|
</section>
|
|
55
44
|
|
|
56
45
|
<NewsletterSignup />
|
|
57
46
|
</BaseLayout>
|
|
47
|
+
|
|
48
|
+
<script>
|
|
49
|
+
import '../lib/client.js'
|
|
50
|
+
import { NymbalCategoryNav } from '@nymbalhq/web-components/elements/nymbal-category-nav'
|
|
51
|
+
import { NymbalProductGrid } from '@nymbalhq/web-components/elements/nymbal-product-grid'
|
|
52
|
+
|
|
53
|
+
NymbalCategoryNav.define()
|
|
54
|
+
NymbalProductGrid.define()
|
|
55
|
+
</script>
|
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
26.12.1
|
|
@@ -1,6 +1,26 @@
|
|
|
1
|
+
import { fileURLToPath } from 'node:url'
|
|
1
2
|
import { defineConfig } from 'astro/config'
|
|
2
3
|
import node from '@astrojs/node'
|
|
3
4
|
|
|
5
|
+
const OWN_ELEMENTS = fileURLToPath(new URL('./src/elements/index.ts', import.meta.url))
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* NYMBAL_ELEMENTS_ENTRY (an absolute path) is the store's elements entry in place of src/elements/index.ts, on the
|
|
9
|
+
* server and in the browser (the E2E override fixture).
|
|
10
|
+
*/
|
|
11
|
+
function elementsEntry(entry) {
|
|
12
|
+
return {
|
|
13
|
+
name: 'nymbal-elements-entry',
|
|
14
|
+
enforce: 'pre',
|
|
15
|
+
async resolveId(source, importer, options) {
|
|
16
|
+
if (importer === undefined || importer === entry) return null
|
|
17
|
+
const resolved = await this.resolve(source, importer, { ...options, skipSelf: true })
|
|
18
|
+
return resolved?.id === OWN_ELEMENTS ? entry : null
|
|
19
|
+
},
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
const elementsEntryPath = process.env.NYMBAL_ELEMENTS_ENTRY
|
|
23
|
+
|
|
4
24
|
// NYMBAL_STOREFRONT_PORT lets `nymbal dev` (and a developer) pick a free port
|
|
5
25
|
// when the default 4321 is taken by another project. The `astro dev --port`
|
|
6
26
|
// flag still overrides this, so `nymbal dev` (which passes --port explicitly)
|
|
@@ -11,4 +31,7 @@ export default defineConfig({
|
|
|
11
31
|
output: 'server',
|
|
12
32
|
adapter: node({ mode: 'standalone' }),
|
|
13
33
|
server: { port },
|
|
34
|
+
// The page's CSS travels in its HTML, so the first paint waits on no stylesheet request (perf-budgets.json: LCP).
|
|
35
|
+
build: { inlineStylesheets: 'always' },
|
|
36
|
+
vite: { plugins: elementsEntryPath ? [elementsEntry(elementsEntryPath)] : [] },
|
|
14
37
|
})
|
|
@@ -17,42 +17,42 @@
|
|
|
17
17
|
"seed": "nymbal seed"
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"nymbal": "1.
|
|
21
|
-
"@nymbalhq/config": "1.
|
|
22
|
-
"@nymbalhq/platform": "1.
|
|
23
|
-
"@nymbalhq/stack": "1.
|
|
20
|
+
"nymbal": "1.2.0",
|
|
21
|
+
"@nymbalhq/config": "1.2.0",
|
|
22
|
+
"@nymbalhq/platform": "1.2.0",
|
|
23
|
+
"@nymbalhq/stack": "1.2.0",
|
|
24
24
|
"@astrojs/node": "^11.1.8",
|
|
25
|
-
"@nymbalhq/sdk": "1.
|
|
26
|
-
"@nymbalhq/types": "1.
|
|
27
|
-
"@nymbalhq/web-components": "1.
|
|
25
|
+
"@nymbalhq/sdk": "1.2.0",
|
|
26
|
+
"@nymbalhq/types": "1.2.0",
|
|
27
|
+
"@nymbalhq/web-components": "1.2.0",
|
|
28
28
|
"astro": "^7.4.0"
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|
|
31
31
|
"@astrojs/check": "^0.9.10",
|
|
32
|
-
"typescript": "
|
|
32
|
+
"typescript": "npm:@typescript/typescript6@^6.0.2"
|
|
33
33
|
},
|
|
34
34
|
"pnpm": {
|
|
35
35
|
"overrides": {
|
|
36
|
-
"nymbal": "1.
|
|
37
|
-
"@nymbalhq/config": "1.
|
|
38
|
-
"@nymbalhq/http": "1.
|
|
39
|
-
"@nymbalhq/platform": "1.
|
|
40
|
-
"@nymbalhq/react": "1.
|
|
41
|
-
"@nymbalhq/sdk": "1.
|
|
42
|
-
"@nymbalhq/stack": "1.
|
|
43
|
-
"@nymbalhq/types": "1.
|
|
44
|
-
"@nymbalhq/web-components": "1.
|
|
36
|
+
"nymbal": "1.2.0",
|
|
37
|
+
"@nymbalhq/config": "1.2.0",
|
|
38
|
+
"@nymbalhq/http": "1.2.0",
|
|
39
|
+
"@nymbalhq/platform": "1.2.0",
|
|
40
|
+
"@nymbalhq/react": "1.2.0",
|
|
41
|
+
"@nymbalhq/sdk": "1.2.0",
|
|
42
|
+
"@nymbalhq/stack": "1.2.0",
|
|
43
|
+
"@nymbalhq/types": "1.2.0",
|
|
44
|
+
"@nymbalhq/web-components": "1.2.0"
|
|
45
45
|
}
|
|
46
46
|
},
|
|
47
47
|
"overrides": {
|
|
48
|
-
"nymbal": "1.
|
|
49
|
-
"@nymbalhq/config": "1.
|
|
50
|
-
"@nymbalhq/http": "1.
|
|
51
|
-
"@nymbalhq/platform": "1.
|
|
52
|
-
"@nymbalhq/react": "1.
|
|
53
|
-
"@nymbalhq/sdk": "1.
|
|
54
|
-
"@nymbalhq/stack": "1.
|
|
55
|
-
"@nymbalhq/types": "1.
|
|
56
|
-
"@nymbalhq/web-components": "1.
|
|
48
|
+
"nymbal": "1.2.0",
|
|
49
|
+
"@nymbalhq/config": "1.2.0",
|
|
50
|
+
"@nymbalhq/http": "1.2.0",
|
|
51
|
+
"@nymbalhq/platform": "1.2.0",
|
|
52
|
+
"@nymbalhq/react": "1.2.0",
|
|
53
|
+
"@nymbalhq/sdk": "1.2.0",
|
|
54
|
+
"@nymbalhq/stack": "1.2.0",
|
|
55
|
+
"@nymbalhq/types": "1.2.0",
|
|
56
|
+
"@nymbalhq/web-components": "1.2.0"
|
|
57
57
|
}
|
|
58
58
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
+
// In a landmark of its own: the header's links are in its shadow root, where a checker cannot tell the skip link
|
|
3
|
+
// comes before them, so it would count it as content outside every landmark.
|
|
2
4
|
---
|
|
3
|
-
<a href="#main" class="skip-nav">Skip to main content</a>
|
|
5
|
+
<nav aria-label="Skip link"><a href="#main" class="skip-nav">Skip to main content</a></nav>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// This store's own elements. Everything here runs before any Nymbal element is defined (src/lib/client.ts runs it
|
|
2
|
+
// first, and every page script imports that), and Nymbal leaves alone any tag that is already defined, so an element
|
|
3
|
+
// defined here under a Nymbal tag replaces Nymbal's. The server renders Nymbal's elements too, so a replacement is
|
|
4
|
+
// also named in `serverElements`, and the server renders it under the tag (src/lib/server-elements.ts). Import
|
|
5
|
+
// @nymbalhq/web-components/hydrate-support before lit, so the element takes over what the server rendered:
|
|
6
|
+
//
|
|
7
|
+
// import '@nymbalhq/web-components/hydrate-support'
|
|
8
|
+
// import { MyMiniCart } from './my-mini-cart.js'
|
|
9
|
+
// if (typeof window !== 'undefined') customElements.define('nymbal-mini-cart', MyMiniCart)
|
|
10
|
+
// export const serverElements = { 'nymbal-mini-cart': MyMiniCart }
|
|
11
|
+
export const serverElements = {}
|
|
@@ -1,17 +1,24 @@
|
|
|
1
1
|
---
|
|
2
2
|
import '../styles/global.css'
|
|
3
|
+
import NymbalElement from '@nymbalhq/web-components/astro'
|
|
3
4
|
import SkipNav from '../components/SkipNav.astro'
|
|
4
|
-
import
|
|
5
|
-
import
|
|
5
|
+
import { FOOTER, HEADER, headerSlots } from '../lib/chrome'
|
|
6
|
+
import { fetchCapabilities, fetchCart, type CartProjection } from '../lib/api'
|
|
6
7
|
|
|
7
8
|
interface Props {
|
|
8
9
|
title: string
|
|
9
10
|
description?: string
|
|
10
11
|
ogImage?: string
|
|
11
12
|
jsonLd?: Record<string, unknown>
|
|
13
|
+
/** The shopper's cart, when the page has read it already (fetchCart); the layout reads it otherwise. */
|
|
14
|
+
cart?: CartProjection | null
|
|
12
15
|
}
|
|
13
16
|
|
|
14
17
|
const { title, description = 'Shop the Nymbal store', ogImage, jsonLd } = Astro.props
|
|
18
|
+
const cart =
|
|
19
|
+
Astro.props.cart !== undefined ? Astro.props.cart : await fetchCart(Astro.request.headers.get('cookie'))
|
|
20
|
+
const capabilities = await fetchCapabilities()
|
|
21
|
+
const canSearch = capabilities?.search.capabilities.includes('search') ?? false
|
|
15
22
|
const canonicalUrl = Astro.url.href
|
|
16
23
|
---
|
|
17
24
|
<!doctype html>
|
|
@@ -22,6 +29,8 @@ const canonicalUrl = Astro.url.href
|
|
|
22
29
|
<title>{title}</title>
|
|
23
30
|
<meta name="description" content={description} />
|
|
24
31
|
<link rel="canonical" href={canonicalUrl} />
|
|
32
|
+
<!-- A page's own head links, early: the product page preloads its LCP image here. -->
|
|
33
|
+
<slot name="head" />
|
|
25
34
|
|
|
26
35
|
<!-- Open Graph -->
|
|
27
36
|
<meta property="og:title" content={title} />
|
|
@@ -37,11 +46,13 @@ const canonicalUrl = Astro.url.href
|
|
|
37
46
|
</head>
|
|
38
47
|
<body>
|
|
39
48
|
<SkipNav />
|
|
40
|
-
<
|
|
49
|
+
<NymbalElement tag="nymbal-site-header" attributes={HEADER}>
|
|
50
|
+
<Fragment set:html={headerSlots(canSearch, cart?.itemCount)} />
|
|
51
|
+
</NymbalElement>
|
|
41
52
|
<main id="main">
|
|
42
53
|
<slot />
|
|
43
54
|
</main>
|
|
44
|
-
<
|
|
55
|
+
<NymbalElement tag="nymbal-site-footer" attributes={FOOTER} />
|
|
45
56
|
|
|
46
57
|
<!-- Web component overlays (rendered outside main flow) -->
|
|
47
58
|
<nymbal-cart-drawer></nymbal-cart-drawer>
|
|
@@ -1,6 +1,11 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
DenormalisedProduct,
|
|
3
|
+
ProductListResult,
|
|
4
|
+
ProductSearchResult,
|
|
5
|
+
ReviewsListResult,
|
|
6
|
+
} from '@nymbalhq/sdk'
|
|
2
7
|
import { DEFAULT_PRODUCT_SORT, PRODUCT_SORT_KEYS } from '@nymbalhq/types'
|
|
3
|
-
import type { Category, Order, ProductSortKey, StoreCapabilities } from '@nymbalhq/types'
|
|
8
|
+
import type { Cart, Category, Order, ProductSortKey, StoreCapabilities } from '@nymbalhq/types'
|
|
4
9
|
|
|
5
10
|
const API_URL = import.meta.env.NYMBAL_API_URL ?? 'http://localhost:3001'
|
|
6
11
|
|
|
@@ -27,13 +32,34 @@ export async function fetchProducts(params?: {
|
|
|
27
32
|
}
|
|
28
33
|
}
|
|
29
34
|
|
|
35
|
+
/** The API failed: it did not answer, answered an error other than a 404, or answered with no data. */
|
|
36
|
+
export class ApiUnavailableError extends Error {
|
|
37
|
+
override name = 'ApiUnavailableError'
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** `GET <path>`'s data; null on a 404; rejects with ApiUnavailableError (or fetch's own error) on any other failure. */
|
|
41
|
+
async function apiData<T>(path: string): Promise<T | null> {
|
|
42
|
+
const res = await fetch(`${API_URL}${path}`)
|
|
43
|
+
if (res.status === 404) return null
|
|
44
|
+
if (!res.ok) throw new ApiUnavailableError(`GET ${path} answered ${res.status}`)
|
|
45
|
+
const envelope = (await res.json()) as { data?: T | null }
|
|
46
|
+
if (envelope.data === undefined || envelope.data === null) {
|
|
47
|
+
throw new ApiUnavailableError(`GET ${path} answered no data`)
|
|
48
|
+
}
|
|
49
|
+
return envelope.data
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** What the store can do, as the API sent it; rejects when the API fails. */
|
|
53
|
+
export async function loadCapabilities(): Promise<StoreCapabilities> {
|
|
54
|
+
const capabilities = await apiData<StoreCapabilities>('/api/capabilities')
|
|
55
|
+
if (capabilities === null) throw new ApiUnavailableError('GET /api/capabilities answered 404')
|
|
56
|
+
return capabilities
|
|
57
|
+
}
|
|
58
|
+
|
|
30
59
|
/** What the store can do, which decides what the page renders; null when the API did not answer. */
|
|
31
60
|
export async function fetchCapabilities(): Promise<StoreCapabilities | null> {
|
|
32
61
|
try {
|
|
33
|
-
|
|
34
|
-
if (!res.ok) return null
|
|
35
|
-
const envelope = (await res.json()) as { data: StoreCapabilities }
|
|
36
|
-
return envelope.data ?? null
|
|
62
|
+
return await loadCapabilities()
|
|
37
63
|
} catch (err) {
|
|
38
64
|
console.error('[api] fetchCapabilities failed', err)
|
|
39
65
|
return null
|
|
@@ -47,34 +73,37 @@ export function sortFromUrl(url: URL, catalog?: StoreCapabilities['catalog']): P
|
|
|
47
73
|
return keys.find((key) => key === sort) ?? catalog?.defaultSort ?? DEFAULT_PRODUCT_SORT
|
|
48
74
|
}
|
|
49
75
|
|
|
76
|
+
/** A product as the API sent it; null when the API has none (a 404); rejects when the API fails. */
|
|
77
|
+
export async function loadProduct(slug: string): Promise<DenormalisedProduct | null> {
|
|
78
|
+
return apiData<DenormalisedProduct>(`/api/products/${encodeURIComponent(slug)}`)
|
|
79
|
+
}
|
|
80
|
+
|
|
50
81
|
export async function fetchProduct(slug: string): Promise<DenormalisedProduct | null> {
|
|
51
82
|
try {
|
|
52
|
-
|
|
53
|
-
if (!res.ok) return null
|
|
54
|
-
const envelope = (await res.json()) as { data: DenormalisedProduct }
|
|
55
|
-
return envelope.data ?? null
|
|
83
|
+
return await loadProduct(slug)
|
|
56
84
|
} catch (err) {
|
|
57
85
|
console.error('[api] fetchProduct failed', err)
|
|
58
86
|
return null
|
|
59
87
|
}
|
|
60
88
|
}
|
|
61
89
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
90
|
+
/** The store's categories as the API sent them; rejects when the API fails. */
|
|
91
|
+
export async function loadCategories(): Promise<Category[]> {
|
|
92
|
+
const categories = await apiData<Category[]>('/api/categories')
|
|
93
|
+
if (categories === null) throw new ApiUnavailableError('GET /api/categories answered 404')
|
|
94
|
+
return categories
|
|
95
|
+
}
|
|
68
96
|
|
|
97
|
+
/**
|
|
98
|
+
* The store's categories, or none when the API failed: a page renders per request and no shared cache keeps it, so
|
|
99
|
+
* the next request shows the store's own; a list the API did not send would link to categories the store lacks.
|
|
100
|
+
*/
|
|
69
101
|
export async function fetchCategories(): Promise<Category[]> {
|
|
70
102
|
try {
|
|
71
|
-
|
|
72
|
-
if (!res.ok) return SEED_CATEGORIES
|
|
73
|
-
const envelope = (await res.json()) as { data: Category[] }
|
|
74
|
-
return envelope.data ?? SEED_CATEGORIES
|
|
103
|
+
return await loadCategories()
|
|
75
104
|
} catch (err) {
|
|
76
105
|
console.error('[api] fetchCategories failed', err)
|
|
77
|
-
return
|
|
106
|
+
return []
|
|
78
107
|
}
|
|
79
108
|
}
|
|
80
109
|
|
|
@@ -101,3 +130,66 @@ export async function fetchReviews(productId: string): Promise<ReviewsListResult
|
|
|
101
130
|
return { items: [], nextCursor: null }
|
|
102
131
|
}
|
|
103
132
|
}
|
|
133
|
+
|
|
134
|
+
/** The cart the cart elements take as their `cart` attribute: its lines and sums, never the cart token. */
|
|
135
|
+
export interface CartProjection {
|
|
136
|
+
items: Cart['items']
|
|
137
|
+
subtotalMinor: number
|
|
138
|
+
currency: string
|
|
139
|
+
itemCount: number
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const CART_COOKIE = 'nymbal.cart'
|
|
143
|
+
|
|
144
|
+
/** The `nymbal.cart` value in a request's Cookie header, or null. */
|
|
145
|
+
function cartToken(cookie: string | null): string | null {
|
|
146
|
+
for (const pair of (cookie ?? '').split(';')) {
|
|
147
|
+
const [name, ...value] = pair.trim().split('=')
|
|
148
|
+
if (name === CART_COOKIE && value.length > 0) return value.join('=')
|
|
149
|
+
}
|
|
150
|
+
return null
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* The shopper's cart, read with the request's `nymbal.cart` cookie, as the projection the elements take. Null without
|
|
155
|
+
* the cookie: `GET /api/cart` with none would make a cart the page could not keep, so the element renders its shell
|
|
156
|
+
* and the browser fills it. The cookie reaches this server when the store and the API share a host or a proxy.
|
|
157
|
+
*/
|
|
158
|
+
export async function fetchCart(cookie: string | null): Promise<CartProjection | null> {
|
|
159
|
+
const token = cartToken(cookie)
|
|
160
|
+
if (token === null) return null
|
|
161
|
+
try {
|
|
162
|
+
const res = await fetch(`${API_URL}/api/cart`, {
|
|
163
|
+
headers: { Cookie: `${CART_COOKIE}=${token}` },
|
|
164
|
+
})
|
|
165
|
+
if (!res.ok) return null
|
|
166
|
+
const { items, subtotalMinor, currency } = ((await res.json()) as { data: Cart }).data
|
|
167
|
+
return {
|
|
168
|
+
items,
|
|
169
|
+
subtotalMinor,
|
|
170
|
+
currency,
|
|
171
|
+
itemCount: items.reduce((n, item) => n + item.qty, 0),
|
|
172
|
+
}
|
|
173
|
+
} catch (err) {
|
|
174
|
+
console.error('[api] fetchCart failed', err)
|
|
175
|
+
return null
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** The search the search page shows for `q` (and its category), as the search store would hold it. */
|
|
180
|
+
export async function fetchSearch(
|
|
181
|
+
q: string,
|
|
182
|
+
category: string | null,
|
|
183
|
+
): Promise<ProductSearchResult | null> {
|
|
184
|
+
const url = new URL('/api/products/search', API_URL)
|
|
185
|
+
url.searchParams.set('q', q)
|
|
186
|
+
if (category) url.searchParams.set('category', category)
|
|
187
|
+
try {
|
|
188
|
+
const res = await fetch(url.toString())
|
|
189
|
+
if (!res.ok) return null
|
|
190
|
+
return ((await res.json()) as { data: ProductSearchResult }).data ?? null
|
|
191
|
+
} catch (err) {
|
|
192
|
+
console.error('[api] fetchSearch failed', err)
|
|
193
|
+
return null
|
|
194
|
+
}
|
|
195
|
+
}
|