@propeller-commerce/create-propeller-shop 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/bin/create-propeller-shop.js +733 -0
- package/dist/bin/create-propeller-shop.js.map +1 -0
- package/dist/bin/propeller.js +203 -0
- package/dist/bin/propeller.js.map +1 -0
- package/dist/chunk-UMI3HB67.js +129 -0
- package/dist/chunk-UMI3HB67.js.map +1 -0
- package/package.json +50 -0
- package/templates/shop-next/b2c-trim.json +11 -0
- package/templates/shop-next/overlay/README.template.md +86 -0
- package/templates/shop-next/overlay/package.patch.json +9 -0
- package/templates/shop-next/template.json +11 -0
- package/templates/shop-nuxt/b2c-trim.json +11 -0
- package/templates/shop-nuxt/overlay/README.template.md +98 -0
- package/templates/shop-nuxt/overlay/package.patch.json +9 -0
- package/templates/shop-nuxt/template.json +11 -0
- package/templates/shop-vue/b2c-trim.json +11 -0
- package/templates/shop-vue/overlay/README.template.md +83 -0
- package/templates/shop-vue/overlay/package.patch.json +9 -0
- package/templates/shop-vue/template.json +11 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/bin/create-propeller-shop.ts","../../src/commands/scaffold.ts","../../src/prompts/index.ts","../../src/template/substitute.ts","../../src/template/clone.ts","../../src/template/jsonPatch.ts","../../src/template/cmsReadme.ts"],"sourcesContent":["/**\n * `create-propeller-shop` — scaffold a new Propeller Commerce shop.\n *\n * npx create-propeller-shop@latest my-shop --stack=next --mode=hybrid --cms=strapi\n *\n * Any missing flag is asked interactively. `--yes` skips confirmations and\n * uses defaults where prompts would have run.\n */\n\nimport { Command } from 'commander';\nimport chalk from 'chalk';\nimport { runScaffold, type ScaffoldOptions } from '../commands/scaffold';\nimport { getCliVersion } from '../util/version';\n\nasync function main(): Promise<void> {\n const cli = new Command();\n const version = await getCliVersion();\n\n cli\n .name('create-propeller-shop')\n .description('Scaffold a Propeller Commerce shop.')\n .version(version)\n .argument('[name]', 'Shop name (kebab-case)')\n .option('--stack <stack>', 'Frontend stack: next | vue | nuxt')\n .option('--mode <mode>', 'Shop mode: b2b | b2c | hybrid')\n .option('--cms <cms>', 'CMS adapter: strapi | cms | none')\n .option('--locales <list>', 'Comma-separated locale list (e.g. en,nl)')\n .option('--default-locale <code>', 'Default locale')\n .option('--currency-code <iso>', 'ISO 4217 currency code (e.g. EUR)')\n .option('--portal-mode <mode>', 'open | semi-closed | closed')\n .option('--site-url <url>', 'Public site origin (no trailing slash)')\n .option('--skip-install', 'Skip npm install after scaffolding')\n .option('-y, --yes', 'Accept defaults for non-critical prompts')\n .action(async (name: string | undefined, options: Record<string, unknown>) => {\n const scaffoldOpts: ScaffoldOptions = {\n name: name ?? undefined,\n stack: options.stack as 'next' | 'vue' | 'nuxt' | undefined,\n mode: options.mode as 'b2b' | 'b2c' | 'hybrid' | undefined,\n cms: options.cms as 'strapi' | 'cms' | 'none' | undefined,\n locales: options.locales\n ? String(options.locales).split(',').map((s) => s.trim())\n : undefined,\n defaultLocale: options.defaultLocale as string | undefined,\n currencyCode: options.currencyCode as string | undefined,\n portalMode: options.portalMode as 'open' | 'semi-closed' | 'closed' | undefined,\n siteUrl: options.siteUrl as string | undefined,\n skipInstall: options.skipInstall === true,\n yes: options.yes === true,\n };\n try {\n await runScaffold(scaffoldOpts);\n } catch (err) {\n // eslint-disable-next-line no-console\n console.error(chalk.red(`\\nScaffold failed: ${(err as Error).message}`));\n process.exit(1);\n }\n });\n\n await cli.parseAsync(process.argv);\n}\n\nmain().catch((err) => {\n // eslint-disable-next-line no-console\n console.error(chalk.red(`Unexpected error: ${(err as Error).message}`));\n process.exit(1);\n});\n","/**\n * `create-propeller-shop <name>` command implementation.\n *\n * Atomic per attempt: any failure rolls back the target directory so the\n * user never lands on a half-scaffolded shop.\n */\n\nimport { promises as fs } from 'node:fs';\nimport * as path from 'node:path';\nimport chalk from 'chalk';\nimport ora from 'ora';\nimport { execa } from 'execa';\nimport { collectShopConfig, type PromptDefaults, type ShopConfig } from '../prompts';\nimport {\n PropellerJsonSchema,\n buildPropellerJson,\n} from '../schema/propellerJson';\nimport { buildContext, type SubstitutionContext } from '../template/substitute';\nimport { scaffoldFromBoilerplate } from '../template/clone';\nimport { buildCmsReadme } from '../template/cmsReadme';\nimport { getCliVersion } from '../util/version';\n\nexport interface ScaffoldOptions extends PromptDefaults {\n yes?: boolean;\n cwd?: string;\n}\n\nexport async function runScaffold(opts: ScaffoldOptions): Promise<void> {\n const cwd = opts.cwd ?? process.cwd();\n\n // 1. Collect config (mix of CLI flags + prompts).\n const config = await collectShopConfig(opts);\n\n // 2. Validate destination.\n const root = path.resolve(cwd, config.name);\n const frontend = path.join(root, 'frontend');\n const cmsFolder = path.join(root, 'cms');\n\n if (await pathExists(root)) {\n throw new Error(\n `Destination \"${root}\" already exists. Pick a different name or remove the existing folder.`\n );\n }\n\n // 3. Scaffold inside a try/rollback frame.\n const spinner = ora({ text: `Scaffolding ${config.name}…`, color: 'cyan' }).start();\n try {\n await fs.mkdir(frontend, { recursive: true });\n await fs.mkdir(cmsFolder, { recursive: true });\n\n const templateVersion = await getCliVersion();\n const ctx: SubstitutionContext = buildContext(config, templateVersion);\n\n // 4. Clone the upstream boilerplate, then apply our thin overlay\n // of templated files. This replaces the previous approach of keeping\n // a stale copy of each boilerplate inside templates/shop-*/shared/ —\n // that model required manual sync every time the boilerplate moved\n // and silently produced out-of-date shops between syncs.\n spinner.text = 'Cloning boilerplate…';\n const copyStats = await scaffoldFromBoilerplate({\n stack: config.stack,\n mode: config.mode,\n destFrontend: frontend,\n ctx,\n });\n\n // 5. Write propeller.json.\n const propellerJson = buildPropellerJson({\n templateName: `propeller-shop-template-${config.stack}`,\n templateVersion,\n stack: config.stack,\n shopName: config.name,\n mode: config.mode,\n locales: config.locales,\n defaultLocale: config.defaultLocale,\n currency: config.currency,\n currencyCode: config.currencyCode,\n portalMode: config.portalMode,\n siteUrl: config.siteUrl,\n cmsAdapter: config.cmsAdapter,\n });\n // Re-parse to confirm we emit a valid manifest.\n PropellerJsonSchema.parse(propellerJson);\n // Escape every non-ASCII code-point to its `\\uXXXX` form so the file is\n // pure ASCII on disk — currency symbols (€, £, ¥, …) render correctly in\n // every terminal / PR-review tool regardless of the host's default\n // encoding (PowerShell on Windows defaults to cp1252 and would otherwise\n // show multibyte UTF-8 glyphs as mojibake like `€`). `JSON.parse`\n // decodes `\\uXXXX` transparently — runtime value is unchanged. Chosen\n // over a UTF-8 BOM because Node's `JSON.parse` does NOT skip a leading\n // BOM (so a BOM would break `propeller doctor` and downstream readers).\n const asciiSafeJson = JSON.stringify(propellerJson, null, 2).replace(\n /[\\u0080-\\uFFFF]/g,\n (c) => '\\\\u' + c.charCodeAt(0).toString(16).toUpperCase().padStart(4, '0')\n );\n await fs.writeFile(\n path.join(frontend, 'propeller.json'),\n asciiSafeJson + '\\n',\n 'utf8'\n );\n\n // 6. Write the CMS folder README.\n await fs.writeFile(\n path.join(cmsFolder, 'README.md'),\n buildCmsReadme(config.cmsAdapter, config.name),\n 'utf8'\n );\n\n spinner.succeed(\n `Scaffolded ${config.name} — ${copyStats.filesCloned} files from ` +\n `boilerplate@${copyStats.upstreamCommit.slice(0, 8)} ` +\n `+ ${copyStats.filesOverlaid + copyStats.filesTemplated} overlay ` +\n `(${copyStats.filesTemplated} templated)` +\n (copyStats.filesTrimmed\n ? ` − ${copyStats.filesTrimmed} b2c-trimmed`\n : '') +\n '.'\n );\n } catch (err) {\n spinner.fail(`Scaffolding failed: ${(err as Error).message}`);\n await safeRemove(root);\n throw err;\n }\n\n // 7. Optional: npm install.\n if (!config.skipInstall) {\n const installSpinner = ora({ text: 'Running npm install in frontend…' }).start();\n try {\n await execa('npm', ['install'], { cwd: frontend, stdio: 'pipe' });\n installSpinner.succeed('npm install complete.');\n } catch (err) {\n installSpinner.warn(\n `npm install failed — you can re-run it manually. (${(err as Error).message})`\n );\n }\n }\n\n // 8. Optional: git init for the parent shop folder.\n try {\n await execa('git', ['init', '--initial-branch=main'], { cwd: root, stdio: 'pipe' });\n await execa('git', ['add', '.'], { cwd: root, stdio: 'pipe' });\n await execa(\n 'git',\n ['commit', '-m', `Scaffolded with create-propeller-shop@${await getCliVersion()}`],\n { cwd: root, stdio: 'pipe' }\n );\n } catch {\n // Non-fatal — the shop is still usable without git.\n }\n\n // 9. Print next steps.\n printNextSteps(config, root);\n}\n\nfunction printNextSteps(config: ShopConfig, root: string): void {\n const relFrontend = path.join(config.name, 'frontend');\n const relCms = path.join(config.name, 'cms');\n // Env-template filename differs per stack: Next ships `.env.local.example`\n // (Next reads `.env.local`); Vue/Nuxt ship `.env.example` (read `.env`).\n const [envExample, envTarget] =\n config.stack === 'next'\n ? ['.env.local.example', '.env.local']\n : ['.env.example', '.env'];\n // eslint-disable-next-line no-console\n console.log(\n [\n '',\n chalk.bold.green(`✓ ${config.name} is ready.`),\n '',\n chalk.bold('Next steps:'),\n ` 1. cd ${relFrontend}`,\n ` 2. Copy ${envExample} to ${envTarget} and fill in the backend endpoints.`,\n ` 3. npm run dev`,\n '',\n config.cmsAdapter\n ? ` See ${relCms}/README.md to install the ${config.cmsAdapter} backend.`\n : ` No CMS configured — see ${relCms}/README.md to add one later.`,\n '',\n ].join('\\n')\n );\n}\n\nasync function pathExists(p: string): Promise<boolean> {\n try {\n await fs.access(p);\n return true;\n } catch {\n return false;\n }\n}\n\nasync function safeRemove(p: string): Promise<void> {\n try {\n await fs.rm(p, { recursive: true, force: true });\n } catch {\n /* ignore */\n }\n}\n","/**\n * Interactive prompt flow for `create-propeller-shop`.\n *\n * Any value not provided as a CLI flag is asked here. Returns a fully\n * resolved `ShopConfig` ready for scaffolding. Validation lives in the\n * Zod schema; this module only collects input.\n */\n\nimport { input, select, confirm } from '@inquirer/prompts';\nimport type {\n ShopMode,\n PortalMode,\n CmsAdapter,\n Stack,\n} from '../schema/propellerJson';\n\nexport interface PromptDefaults {\n name?: string;\n stack?: Stack;\n mode?: ShopMode;\n cms?: CmsAdapter | 'none';\n locales?: string[];\n defaultLocale?: string;\n currency?: string;\n currencyCode?: string;\n portalMode?: PortalMode;\n siteUrl?: string;\n skipInstall?: boolean;\n /**\n * When true, missing values are filled from built-in defaults instead of\n * prompting. The shop name is still required (no sensible default).\n */\n yes?: boolean;\n}\n\nexport interface ShopConfig {\n name: string;\n stack: Stack;\n mode: ShopMode;\n cmsAdapter: CmsAdapter; // null when --cms=none\n locales: string[];\n defaultLocale: string;\n currency: string;\n currencyCode: string;\n portalMode: PortalMode;\n siteUrl: string;\n skipInstall: boolean;\n}\n\nconst KEBAB = /^[a-z][a-z0-9-]*$/;\n\nfunction defaultPortalForMode(mode: ShopMode): PortalMode {\n return mode === 'b2b' ? 'semi-closed' : 'open';\n}\n\n/** Prompt for everything not already provided, with sensible defaults. */\nexport async function collectShopConfig(defaults: PromptDefaults): Promise<ShopConfig> {\n const yes = defaults.yes === true;\n\n // Name has no sensible default — required either as flag or via prompt.\n const name =\n defaults.name ??\n (await input({\n message: 'Shop name (kebab-case, used as folder + package name):',\n validate: (v) => (KEBAB.test(v) ? true : 'Use lowercase letters, digits, and hyphens; start with a letter.'),\n }));\n\n const stack: Stack =\n defaults.stack ??\n (yes\n ? 'next'\n : await select({\n message: 'Frontend stack?',\n choices: [\n { name: 'Next.js 16 (React)', value: 'next' as const },\n { name: 'Vue 3 + Vite SSR', value: 'vue' as const },\n { name: 'Nuxt 3 (Vue SSR)', value: 'nuxt' as const },\n ],\n }));\n\n const mode: ShopMode =\n defaults.mode ??\n (yes\n ? 'hybrid'\n : await select({\n message: 'Shop mode?',\n choices: [\n { name: 'Hybrid (both Contact and Customer users)', value: 'hybrid' as const },\n { name: 'B2B only (Contacts; semi-closed by default)', value: 'b2b' as const },\n { name: 'B2C only (Customers; open by default)', value: 'b2c' as const },\n ],\n }));\n\n const cmsChoice =\n defaults.cms ??\n (yes\n ? 'none'\n : await select<CmsAdapter | 'none'>({\n message: 'CMS adapter?',\n choices: [\n { name: 'Strapi (open-source headless CMS)', value: 'strapi' },\n { name: 'Generic Propeller CMS', value: 'cms' },\n { name: 'None — homepage falls back to static, marketing slugs return 404', value: 'none' },\n ],\n }));\n const cmsAdapter: CmsAdapter = cmsChoice === 'none' ? null : cmsChoice;\n\n const localesStr =\n defaults.locales?.join(',') ??\n (yes\n ? 'en'\n : await input({\n message: 'Locales (comma-separated BCP-47 codes):',\n default: 'en',\n validate: (v) => (v.split(',').every((s) => s.trim().length > 0) ? true : 'At least one locale required.'),\n }));\n const locales = localesStr.split(',').map((s) => s.trim()).filter(Boolean);\n\n const defaultLocale =\n defaults.defaultLocale ??\n (yes\n ? locales[0]\n : await select({\n message: 'Default locale?',\n choices: locales.map((l) => ({ name: l, value: l })),\n }));\n\n const currencyCode =\n defaults.currencyCode ??\n (yes\n ? 'EUR'\n : await input({\n message: 'Currency code (ISO 4217)?',\n default: 'EUR',\n validate: (v) => (/^[A-Z]{3}$/.test(v) ? true : 'Three uppercase letters, e.g. EUR.'),\n }));\n\n // Sensible default symbol for common codes; user can edit later.\n const currency = defaults.currency ?? defaultCurrencySymbol(currencyCode);\n\n const portalMode: PortalMode =\n defaults.portalMode ??\n (yes\n ? defaultPortalForMode(mode)\n : await select({\n message: 'Portal access mode?',\n choices: [\n { name: 'open (anonymous users see catalog + prices)', value: 'open' as const },\n { name: 'semi-closed (catalog visible, prices hidden until login)', value: 'semi-closed' as const },\n { name: 'closed (login required for anything)', value: 'closed' as const },\n ],\n default: defaultPortalForMode(mode),\n }));\n\n const siteUrl =\n defaults.siteUrl ??\n (yes\n ? `https://${name}.example.com`\n : await input({\n message: 'Site URL (no trailing slash):',\n default: `https://${name}.example.com`,\n validate: (v) => {\n try {\n // eslint-disable-next-line no-new\n new URL(v);\n return v.endsWith('/') ? 'Remove trailing slash.' : true;\n } catch {\n return 'Must be a valid URL.';\n }\n },\n }));\n\n const skipInstall =\n defaults.skipInstall ??\n (yes\n ? false\n : !(await confirm({\n message: 'Run `npm install` now?',\n default: true,\n })));\n\n return {\n name,\n stack,\n mode,\n cmsAdapter,\n locales,\n defaultLocale,\n currency,\n currencyCode,\n portalMode,\n siteUrl,\n skipInstall,\n };\n}\n\nfunction defaultCurrencySymbol(code: string): string {\n switch (code) {\n case 'EUR': return '€';\n case 'USD': return '$';\n case 'GBP': return '£';\n case 'CHF': return 'CHF';\n case 'SEK': case 'NOK': case 'DKK': return 'kr';\n case 'PLN': return 'zł';\n default: return code;\n }\n}\n","/**\n * Handlebars substitution over template files.\n *\n * Files ending in `.template.<ext>` get run through Handlebars and the\n * `.template` suffix is stripped on write. All other files are copied\n * verbatim — this keeps Handlebars off code that doesn't need it and\n * avoids `{{ }}` collisions with JSX or Vue mustache syntax.\n *\n * Variables exposed to templates — keep this list in sync with the\n * `SubstitutionContext` interface below. Categories:\n *\n * Identity\n * shopName, shopDescription, siteUrl, siteHostname, apiHostname\n *\n * Stack & mode\n * stack, shopMode, isB2B, isB2C, isHybrid\n *\n * Locale & currency\n * defaultLocale, defaultLocaleLower, defaultLanguage (2-char form,\n * uppercase — e.g. 'NL' from 'nl_NL'; what Propeller's GraphQL API\n * expects for the `language:` argument), locales (array), localesArray,\n * localesJsonArray, currency, currencyCode\n *\n * Portal & integration\n * portalMode, portalModeConstant, channelId, anonymousId, taxZone\n *\n * CMS\n * cmsAdapter 'strapi' | 'cms' | null (truthy boolean via #if)\n * cmsAdapterTsValue \"'strapi'\" / \"'cms'\" / 'null' — TS literal\n * cmsAdapterJsonValue '\"strapi\"' / '\"cms\"' / 'null' — JSON literal\n * cmsAdapterPackage full npm package name or ''\n * cmsAdapterDisplay human-readable, e.g. 'Strapi' / 'none'\n *\n * Mode-derived UI defaults\n * showUserType \"'Contact'\" | \"'Customer'\" | 'null'\n *\n * Feature flags (mode-derived defaults)\n * featureQuotes, featureAuthorization, featureContacts (booleans)\n *\n * Versioning\n * templateVersion\n */\n\nimport Handlebars from 'handlebars';\nimport type { ShopConfig } from '../prompts';\n\nexport interface SubstitutionContext {\n // Identity\n shopName: string;\n shopDescription: string;\n siteUrl: string;\n siteHostname: string;\n apiHostname: string;\n\n // Stack & mode\n stack: string;\n shopMode: string;\n isB2B: boolean;\n isB2C: boolean;\n isHybrid: boolean;\n\n // Locale & currency\n defaultLocale: string;\n defaultLocaleLower: string;\n /**\n * 2-char uppercase language code derived from `defaultLocale` — e.g.\n * 'NL' from 'nl_NL'. Propeller's GraphQL API rejects anything other\n * than a 2-char code on the `language:` argument (\"Language has to be\n * valid language code with 2 characters\"), so templates must use this\n * for any backend-bound default rather than `defaultLocale` itself.\n */\n defaultLanguage: string;\n locales: string[];\n localesArray: string;\n localesJsonArray: string;\n /** Locales minus the default, as a JSON array string. Drives URL-prefix\n * rewrite tables (e.g. proxy.ts) where the default locale gets no prefix. */\n nonDefaultLocalesJsonArray: string;\n currency: string;\n currencyCode: string;\n\n // Portal & integration\n portalMode: string;\n portalModeConstant: string;\n channelId: number;\n anonymousId: number;\n taxZone: string;\n\n // CMS\n cmsAdapter: string | null;\n cmsAdapterTsValue: string;\n cmsAdapterJsonValue: string;\n cmsAdapterPackage: string;\n cmsAdapterDisplay: string;\n\n // Mode-derived UI defaults\n showUserType: string;\n\n // Feature flags\n featureQuotes: boolean;\n featureAuthorization: boolean;\n featureContacts: boolean;\n\n // Versioning\n templateVersion: string;\n}\n\nexport function buildContext(\n config: ShopConfig,\n templateVersion: string\n): SubstitutionContext {\n const siteUrlObj = (() => {\n try {\n return new URL(config.siteUrl);\n } catch {\n return null;\n }\n })();\n\n const siteHostname = siteUrlObj?.hostname ?? '';\n // Heuristic: `api.<root-of-site-domain>` for image patterns. Shop owners\n // can edit `next.config.ts` after scaffolding if their backend lives\n // elsewhere.\n const apiHostname = siteHostname.split('.').slice(-2).join('.') || 'example.com';\n\n return {\n // Identity\n shopName: config.name,\n shopDescription: `${config.name} — powered by Propeller Commerce`,\n siteUrl: config.siteUrl,\n siteHostname,\n apiHostname,\n\n // Stack & mode\n stack: config.stack,\n shopMode: config.mode,\n isB2B: config.mode === 'b2b',\n isB2C: config.mode === 'b2c',\n isHybrid: config.mode === 'hybrid',\n\n // Locale & currency\n defaultLocale: config.defaultLocale,\n defaultLocaleLower: config.defaultLocale.toLowerCase(),\n defaultLanguage: config.defaultLocale.split(/[_-]/)[0].toUpperCase(),\n locales: config.locales,\n localesArray: JSON.stringify(config.locales),\n localesJsonArray: JSON.stringify(config.locales),\n nonDefaultLocalesJsonArray: JSON.stringify(\n config.locales.filter((l) => l.toLowerCase() !== config.defaultLocale.toLowerCase())\n ),\n currency: config.currency,\n currencyCode: config.currencyCode,\n\n // Portal & integration\n portalMode: config.portalMode,\n portalModeConstant: portalModeConstantFor(config.portalMode),\n channelId: 621,\n anonymousId: 71,\n taxZone: 'NL',\n\n // CMS\n cmsAdapter: config.cmsAdapter,\n cmsAdapterTsValue: cmsAdapterTsValueFor(config.cmsAdapter),\n cmsAdapterJsonValue: cmsAdapterJsonValueFor(config.cmsAdapter),\n cmsAdapterPackage: cmsAdapterPackageFor(config.cmsAdapter),\n cmsAdapterDisplay: config.cmsAdapter ?? 'none',\n\n // Mode-derived UI defaults\n showUserType: showUserTypeFor(config.mode),\n\n // Feature flags — defaults track shopMode; ejectable later.\n featureQuotes: config.mode !== 'b2c',\n featureAuthorization: config.mode !== 'b2c',\n featureContacts: config.mode !== 'b2c',\n\n // Versioning\n templateVersion,\n };\n}\n\nfunction showUserTypeFor(mode: ShopConfig['mode']): string {\n switch (mode) {\n case 'b2b': return \"'Contact'\";\n case 'b2c': return \"'Customer'\";\n case 'hybrid': return 'null';\n }\n}\n\nfunction portalModeConstantFor(mode: ShopConfig['portalMode']): string {\n switch (mode) {\n case 'open': return 'OPEN';\n case 'semi-closed': return 'SEMI_CLOSED';\n case 'closed': return 'CLOSED';\n }\n}\n\nfunction cmsAdapterTsValueFor(adapter: ShopConfig['cmsAdapter']): string {\n return adapter === null ? 'null' : `'${adapter}'`;\n}\n\nfunction cmsAdapterJsonValueFor(adapter: ShopConfig['cmsAdapter']): string {\n return adapter === null ? 'null' : JSON.stringify(adapter);\n}\n\nfunction cmsAdapterPackageFor(adapter: ShopConfig['cmsAdapter']): string {\n if (adapter === null) return '';\n return adapter === 'strapi'\n ? 'propeller-v2-cms-adapter-strapi'\n : 'propeller-v2-cms-adapter-cms';\n}\n\n/** Substitute Handlebars variables in `source`. Errors include the variable name. */\nexport function renderTemplate(\n source: string,\n context: SubstitutionContext\n): string {\n const template = Handlebars.compile(source, {\n strict: true, // missing variable → throw with name\n noEscape: true, // template output is code; do not HTML-escape\n });\n return template(context);\n}\n\n/**\n * Decide the destination filename for a copied template file.\n * Strips `.template` from `foo.template.ts` → `foo.ts`.\n */\nexport function stripTemplateSuffix(filename: string): string {\n return filename.replace(/\\.template(?=\\.[^.]+$)/, '');\n}\n\n/** Heuristic: does this filename need Handlebars substitution? */\nexport function isTemplateFile(filename: string): boolean {\n return /\\.template\\.[^.]+$/.test(filename);\n}\n","/**\n * Boilerplate clone-and-overlay scaffolder.\n *\n * The old `templates/shop-<stack>/shared/` approach kept a stale duplicate of each\n * boilerplate inside the accelerator repo. Boilerplate fixes silently fell\n * out of sync until someone manually ported them, and scaffolded shops would\n * miss recent work. This module replaces that with a `git clone --depth 1`\n * of the upstream boilerplate at scaffold time, followed by a thin overlay\n * of templated values (shop name, currency, locales, mode flags).\n *\n * Source-of-truth diagram:\n *\n * git clone --depth 1 <boilerplate> # upstream truth, always fresh\n * └── frontend/ # used as the scaffold base\n * templates/shop-<stack>/overlay/ # accelerator overrides only\n * ├── package.template.json\n * ├── propeller.json placeholder\n * └── (a handful of templated files)\n *\n * The overlay is applied LAST so it overwrites whatever the boilerplate\n * shipped. B2C-mode trimming runs after that to delete account routes that\n * don't apply (quotes / quote-requests / authorization-*).\n */\n\nimport { promises as fs, existsSync } from 'node:fs';\nimport * as path from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport { execa } from 'execa';\nimport {\n isTemplateFile,\n renderTemplate,\n stripTemplateSuffix,\n type SubstitutionContext,\n} from './substitute';\nimport {\n applyJsonPatch,\n isJsonPatch,\n jsonPatchTargetName,\n} from './jsonPatch';\nimport type { ShopConfig } from '../prompts';\n\n/**\n * Upstream boilerplate repositories, keyed by stack.\n *\n * These are the PUBLIC GitHub mirrors (so `npx create-propeller-shop` works\n * for unauthenticated users). Each is a one-way, scrubbed mirror of the\n * private GitLab boilerplate — the mirror CI strips internal dev files\n * (.claude/, CLAUDE.md, memory/, Taskfile.yml) before publishing.\n */\nexport const BOILERPLATE_REPOS: Record<ShopConfig['stack'], string> = {\n next: 'https://github.com/propeller-commerce/propeller-v2-next-boilerplate.git',\n vue: 'https://github.com/propeller-commerce/propeller-v2-vue-boilerplate.git',\n nuxt: 'https://github.com/propeller-commerce/propeller-v2-nuxt-boilerplate.git',\n};\n\n/**\n * Sub-path within the cloned boilerplate that becomes the scaffolded\n * `frontend/`. propeller-vue puts its app code at `frontend/`; the others\n * are flat repos so it's the root.\n */\nconst BOILERPLATE_FRONTEND_SUBPATH: Record<ShopConfig['stack'], string> = {\n next: '.',\n vue: 'frontend',\n nuxt: '.',\n};\n\n/**\n * Default boilerplate branch to clone. Override per-shop via\n * `propeller.json -> boilerplate.ref`. Most consumers want `master`; users\n * trying a bleeding-edge feature can pass `develop` or a tag.\n */\nconst DEFAULT_BOILERPLATE_REF = 'master';\n\n/** Override for local dev: `file:` paths to use already-cloned boilerplates. */\nconst LOCAL_BOILERPLATE_ENV_VARS: Record<ShopConfig['stack'], string> = {\n next: 'PROPELLER_NEXT_BOILERPLATE_LOCAL',\n vue: 'PROPELLER_VUE_BOILERPLATE_LOCAL',\n nuxt: 'PROPELLER_NUXT_BOILERPLATE_LOCAL',\n};\n\nexport interface CloneResult {\n filesCloned: number;\n filesOverlaid: number;\n filesTemplated: number;\n filesPatched: number;\n filesTrimmed: number;\n upstreamRef: string;\n upstreamCommit: string;\n}\n\n/** Resolve the accelerator's `templates/` directory. Identical logic to copy.ts. */\nfunction resolveTemplatesRoot(): string {\n if (process.env.PROPELLER_TEMPLATES_DIR) return process.env.PROPELLER_TEMPLATES_DIR;\n const here = path.dirname(fileURLToPath(import.meta.url));\n const published = path.resolve(here, '..', '..', 'templates');\n if (existsSync(published)) return published;\n const dev = path.resolve(here, '..', '..', '..', '..', 'templates');\n return dev;\n}\n\n/**\n * Step 1: clone the boilerplate into a temp directory, then move its\n * frontend sub-path into the destination.\n *\n * Local override: if `PROPELLER_<STACK>_BOILERPLATE_LOCAL` is set to a\n * directory path, we copy from that directory instead of cloning. Saves\n * minutes during accelerator development and lets you test in-flight\n * boilerplate changes without pushing first.\n */\nasync function cloneBoilerplate(args: {\n stack: ShopConfig['stack'];\n ref: string;\n destFrontend: string;\n}): Promise<{ filesCloned: number; upstreamCommit: string }> {\n const localOverride = process.env[LOCAL_BOILERPLATE_ENV_VARS[args.stack]];\n if (localOverride) {\n const subpath = BOILERPLATE_FRONTEND_SUBPATH[args.stack];\n const src = subpath === '.' ? localOverride : path.join(localOverride, subpath);\n if (!existsSync(src)) {\n throw new Error(\n `Local boilerplate override ${LOCAL_BOILERPLATE_ENV_VARS[args.stack]}=${localOverride} ` +\n `points at ${src} which doesn't exist.`\n );\n }\n const filesCloned = await copyDirVerbatim(src, args.destFrontend);\n return { filesCloned, upstreamCommit: 'local-override' };\n }\n\n const repo = BOILERPLATE_REPOS[args.stack];\n const tmpRoot = await fs.mkdtemp(path.join(\n process.env.TEMP || process.env.TMPDIR || '/tmp',\n `propeller-boilerplate-${args.stack}-`\n ));\n try {\n await execa('git', [\n 'clone',\n '--depth', '1',\n '--branch', args.ref,\n '--single-branch',\n repo,\n tmpRoot,\n ], { stdio: 'pipe' });\n const { stdout: commit } = await execa('git', ['-C', tmpRoot, 'rev-parse', 'HEAD']);\n const subpath = BOILERPLATE_FRONTEND_SUBPATH[args.stack];\n const src = subpath === '.' ? tmpRoot : path.join(tmpRoot, subpath);\n if (!existsSync(src)) {\n throw new Error(\n `Cloned boilerplate at ${repo}#${args.ref} doesn't have a \"${subpath}\" subdirectory.`\n );\n }\n const filesCloned = await copyDirVerbatim(src, args.destFrontend);\n return { filesCloned, upstreamCommit: commit.trim() };\n } finally {\n await fs.rm(tmpRoot, { recursive: true, force: true });\n }\n}\n\n/**\n * Step 2: walk the overlay tree and either copy-as-is or render-and-strip\n * `.template.*` files on top of the cloned boilerplate.\n */\nasync function applyOverlay(args: {\n overlaySrc: string;\n destFrontend: string;\n ctx: SubstitutionContext;\n}): Promise<{ filesOverlaid: number; filesTemplated: number; filesPatched: number }> {\n let filesOverlaid = 0;\n let filesTemplated = 0;\n let filesPatched = 0;\n if (!existsSync(args.overlaySrc)) {\n return { filesOverlaid, filesTemplated, filesPatched };\n }\n await walk(args.overlaySrc, async (entry) => {\n const rel = path.relative(args.overlaySrc, entry.fullPath);\n const destPath = path.join(args.destFrontend, rel);\n if (entry.isDirectory) {\n await fs.mkdir(destPath, { recursive: true });\n return;\n }\n await fs.mkdir(path.dirname(destPath), { recursive: true });\n if (isJsonPatch(entry.name)) {\n const targetName = jsonPatchTargetName(entry.name);\n const targetPath = path.join(path.dirname(destPath), targetName);\n await applyJsonPatch({\n patchPath: entry.fullPath,\n targetPath,\n ctx: args.ctx,\n });\n filesPatched += 1;\n } else if (isTemplateFile(entry.name)) {\n const source = await fs.readFile(entry.fullPath, 'utf8');\n const rendered = renderTemplate(source, args.ctx);\n const finalPath = path.join(\n path.dirname(destPath),\n stripTemplateSuffix(entry.name)\n );\n await fs.writeFile(finalPath, rendered, 'utf8');\n filesTemplated += 1;\n } else {\n await fs.copyFile(entry.fullPath, destPath);\n filesOverlaid += 1;\n }\n });\n return { filesOverlaid, filesTemplated, filesPatched };\n}\n\n/**\n * Step 3: trim the b2c-mode shop. The boilerplate ships every route (quotes,\n * quote-requests, authorization-*) because it's b2b/hybrid by default — a\n * b2c shop has no use for them. `templates/shop-<stack>/b2c-trim.json` lists\n * paths to delete from the cloned tree.\n */\nasync function trimB2C(args: {\n trimManifestPath: string;\n destFrontend: string;\n}): Promise<number> {\n if (!existsSync(args.trimManifestPath)) return 0;\n const raw = await fs.readFile(args.trimManifestPath, 'utf8');\n const manifest = JSON.parse(raw) as { remove: string[] };\n let count = 0;\n for (const rel of manifest.remove) {\n const target = path.join(args.destFrontend, rel);\n if (existsSync(target)) {\n await fs.rm(target, { recursive: true, force: true });\n count += 1;\n }\n }\n return count;\n}\n\n/**\n * Top-level: clone → overlay → trim. Replaces the old `scaffoldTemplateTree`.\n */\nexport async function scaffoldFromBoilerplate(args: {\n stack: ShopConfig['stack'];\n mode: ShopConfig['mode'];\n ref?: string;\n destFrontend: string;\n ctx: SubstitutionContext;\n}): Promise<CloneResult> {\n const ref = args.ref ?? DEFAULT_BOILERPLATE_REF;\n\n const clone = await cloneBoilerplate({\n stack: args.stack,\n ref,\n destFrontend: args.destFrontend,\n });\n\n const templatesRoot = resolveTemplatesRoot();\n const stackRoot = path.join(templatesRoot, `shop-${args.stack}`);\n\n const overlay = await applyOverlay({\n overlaySrc: path.join(stackRoot, 'overlay'),\n destFrontend: args.destFrontend,\n ctx: args.ctx,\n });\n\n let filesTrimmed = 0;\n if (args.mode === 'b2c') {\n filesTrimmed = await trimB2C({\n trimManifestPath: path.join(stackRoot, 'b2c-trim.json'),\n destFrontend: args.destFrontend,\n });\n }\n\n return {\n filesCloned: clone.filesCloned,\n filesOverlaid: overlay.filesOverlaid,\n filesTemplated: overlay.filesTemplated,\n filesPatched: overlay.filesPatched,\n filesTrimmed,\n upstreamRef: ref,\n upstreamCommit: clone.upstreamCommit,\n };\n}\n\n// ── Utilities ───────────────────────────────────────────────────────────────\n\n/**\n * Top-level dirs that the verbatim copy MUST NOT pull from the boilerplate.\n * - `.git`: heavy + confusing (shop owner runs `git init` themselves).\n * - `node_modules`, `.next`, `.nuxt`, `dist`, `.vite`: build / install\n * artifacts; consumer reinstalls anyway.\n * - `playwright-report`, `test-results`, `coverage`: ephemeral test output.\n * - `.claude`, `memory`: dev-only assistant scratch space.\n */\nconst VERBATIM_SKIP_TOP = new Set([\n '.git',\n 'node_modules',\n '.next',\n '.nuxt',\n '.output',\n 'dist',\n '.vite',\n '.turbo',\n 'playwright-report',\n 'test-results',\n 'coverage',\n '.claude',\n 'memory',\n]);\n\n/**\n * Top-level file names that the verbatim copy MUST NOT pull. Mostly env\n * files: when the local-override is used in dev, the upstream's real `.env`\n * (with live API keys) would otherwise leak into the scaffolded shop. The\n * boilerplate's `.env.example` is the SOURCE OF TRUTH for the scaffold;\n * shops fill in real values themselves.\n */\nconst VERBATIM_SKIP_FILE = new Set([\n '.env',\n '.env.local',\n '.env.development',\n '.env.production',\n '.env.development.local',\n '.env.production.local',\n]);\n\n/**\n * Plain recursive copy of a directory tree. Skips heavy/junk top-level\n * directories listed in VERBATIM_SKIP_TOP — both for the write AND the\n * recursion (walking into node_modules is the slow part, not the writes).\n */\nasync function copyDirVerbatim(src: string, dest: string): Promise<number> {\n let count = 0;\n await walkWithSkip(src, async (entry) => {\n const rel = path.relative(src, entry.fullPath);\n const destPath = path.join(dest, rel);\n if (entry.isDirectory) {\n await fs.mkdir(destPath, { recursive: true });\n return;\n }\n await fs.mkdir(path.dirname(destPath), { recursive: true });\n await fs.copyFile(entry.fullPath, destPath);\n count += 1;\n }, VERBATIM_SKIP_TOP);\n return count;\n}\n\ninterface WalkEntry {\n fullPath: string;\n name: string;\n isDirectory: boolean;\n}\n\nconst SKIP_NAMES = new Set(['.gitkeep']);\n\nasync function walk(dir: string, visit: (entry: WalkEntry) => Promise<void>) {\n const entries = await fs.readdir(dir, { withFileTypes: true });\n for (const entry of entries) {\n if (SKIP_NAMES.has(entry.name)) continue;\n const fullPath = path.join(dir, entry.name);\n await visit({ fullPath, name: entry.name, isDirectory: entry.isDirectory() });\n if (entry.isDirectory()) {\n await walk(fullPath, visit);\n }\n }\n}\n\n/**\n * Variant of `walk` that prunes the recursion at any directory matching a\n * skip set. The skip applies to the top-level segment in the path —\n * `node_modules/foo/bar` is pruned but `app/data/node_modules.json` (file\n * with that string in the name) is fine.\n */\nasync function walkWithSkip(\n root: string,\n visit: (entry: WalkEntry) => Promise<void>,\n skipDirNames: Set<string>,\n current?: string,\n) {\n const here = current ?? root;\n const entries = await fs.readdir(here, { withFileTypes: true });\n for (const entry of entries) {\n if (SKIP_NAMES.has(entry.name)) continue;\n // Only apply skip-dir check to directories so a file with the same name\n // (rare) doesn't get pruned. Skip-file applies at the top level only,\n // because nested `.env`-like files are unrelated to the boilerplate's\n // own runtime config.\n if (entry.isDirectory() && skipDirNames.has(entry.name)) continue;\n if (!entry.isDirectory() && here === root && VERBATIM_SKIP_FILE.has(entry.name)) continue;\n const fullPath = path.join(here, entry.name);\n await visit({ fullPath, name: entry.name, isDirectory: entry.isDirectory() });\n if (entry.isDirectory()) {\n await walkWithSkip(root, visit, skipDirNames, fullPath);\n }\n }\n}\n","/**\n * Merge JSON patch files onto an existing JSON file.\n *\n * Overlay model: instead of an overlay `package.json` that REPLACES the\n * boilerplate's `package.json` (which drags the overlay's stale dependency\n * versions over the boilerplate's up-to-date ones — exactly the drift the\n * clone-and-overlay redesign was supposed to fix), we ship overlay patches\n * named `<filename>.patch.json`. The CLI reads the cloned boilerplate's\n * `<filename>.json`, deep-merges the patch on top, and writes the result\n * back. The boilerplate's version always wins for anything the patch\n * doesn't explicitly override.\n *\n * Semantics:\n * - Plain values overwrite.\n * - Arrays REPLACE the boilerplate's array. (Mixing-by-index would be\n * surprising; if you need additive arrays, ask in a code review.)\n * - Nested objects deep-merge.\n * - To DELETE a key (e.g. remove a script from the boilerplate's scripts),\n * set its value to `null` in the patch.\n * - Handlebars tokens (`{{shopName}}`) in the patch are rendered before\n * the merge.\n */\n\nimport { promises as fs } from 'node:fs';\nimport * as path from 'node:path';\nimport { renderTemplate, type SubstitutionContext } from './substitute';\n\nconst PATCH_SUFFIX = '.patch.json';\n\n/** Does this overlay path identify a JSON patch (not a literal file)? */\nexport function isJsonPatch(name: string): boolean {\n return name.endsWith(PATCH_SUFFIX);\n}\n\n/** Strip `.patch.json` and replace with `.json` for the target filename. */\nexport function jsonPatchTargetName(name: string): string {\n return name.slice(0, -PATCH_SUFFIX.length) + '.json';\n}\n\n/**\n * Apply a `.patch.json` overlay to an existing JSON file in the destination.\n * Throws if the target doesn't exist (the boilerplate is supposed to ship\n * the base file — if it doesn't, the overlay author has the wrong assumption).\n */\nexport async function applyJsonPatch(args: {\n patchPath: string;\n targetPath: string;\n ctx: SubstitutionContext;\n}): Promise<void> {\n const patchRaw = await fs.readFile(args.patchPath, 'utf8');\n const patchRendered = renderTemplate(patchRaw, args.ctx);\n let patch: unknown;\n try {\n patch = JSON.parse(patchRendered);\n } catch (e) {\n throw new Error(\n `JSON patch ${args.patchPath} is not valid JSON after rendering: ${(e as Error).message}`\n );\n }\n\n let target: unknown;\n try {\n const targetRaw = await fs.readFile(args.targetPath, 'utf8');\n target = JSON.parse(targetRaw);\n } catch (e) {\n throw new Error(\n `Cannot apply patch ${path.basename(args.patchPath)}: target ${args.targetPath} ` +\n `missing or unparseable. The boilerplate is supposed to ship this file. ` +\n `(${(e as Error).message})`\n );\n }\n\n const merged = deepMerge(target, patch);\n await fs.writeFile(args.targetPath, JSON.stringify(merged, null, 2) + '\\n', 'utf8');\n}\n\n/**\n * Deep-merge `patch` onto `base`. See module doc for semantics.\n * Exported for unit testing.\n */\nexport function deepMerge(base: unknown, patch: unknown): unknown {\n if (!isPlainObject(base) || !isPlainObject(patch)) {\n return patch; // arrays + primitives + mismatched shapes → replace\n }\n const out: Record<string, unknown> = { ...base };\n for (const [key, patchValue] of Object.entries(patch)) {\n if (patchValue === null) {\n delete out[key];\n continue;\n }\n if (isPlainObject(out[key]) && isPlainObject(patchValue)) {\n out[key] = deepMerge(out[key], patchValue);\n } else {\n out[key] = patchValue;\n }\n }\n return out;\n}\n\nfunction isPlainObject(v: unknown): v is Record<string, unknown> {\n return typeof v === 'object' && v !== null && !Array.isArray(v);\n}\n","/**\n * Generate the per-CMS install README that lands in `cms/`.\n *\n * v0.1 ships a small per-adapter snippet so shop owners know which backend\n * to install and which env var the frontend expects. The backend itself\n * is NOT scaffolded by us — see the design note in the plan.\n */\n\nimport type { CmsAdapter } from '../schema/propellerJson';\n\nexport function buildCmsReadme(adapter: CmsAdapter, shopName: string): string {\n const header = `# ${shopName} — CMS\\n\\nThis folder hosts the CMS install for the ${shopName} shop.\\n\\n`;\n switch (adapter) {\n case 'strapi':\n return (\n header +\n `## Install Strapi\\n\\n` +\n `\\`\\`\\`bash\\nnpx create-strapi-app@latest .\\n\\`\\`\\`\\n\\n` +\n `Pick **TypeScript** when prompted; the adapter's mappers assume the\\n` +\n `default content-type shape. Once Strapi runs, set its public URL in\\n` +\n `\\`../frontend/.env.local\\`:\\n\\n` +\n `\\`\\`\\`\\nCMS_URL=http://localhost:1337\\n\\`\\`\\`\\n\\n` +\n `The frontend resolves CMS pages at \\`/{slug}\\` through the catch-all\\n` +\n `route. Pages not found in Strapi return 404; the homepage falls back\\n` +\n `to its built-in static structure when Strapi returns null.\\n`\n );\n case 'cms':\n return (\n header +\n `## Install Propeller CMS\\n\\n` +\n `See the Propeller CMS install guide:\\n` +\n `https://docs.propeller-commerce.com/cms/install\\n\\n` +\n `Once installed, set its public URL in \\`../frontend/.env.local\\`:\\n\\n` +\n `\\`\\`\\`\\nCMS_URL=http://localhost:8080\\n\\`\\`\\`\\n\\n` +\n `The frontend resolves CMS pages at \\`/{slug}\\` through the catch-all\\n` +\n `route. Pages not found in the CMS return 404; the homepage falls\\n` +\n `back to its built-in static structure when the CMS returns null.\\n`\n );\n case null:\n return (\n header +\n `## No CMS configured\\n\\n` +\n `This shop was scaffolded without a CMS. Marketing-content slugs\\n` +\n `(About Us, FAQ, …) will return 404. The homepage uses the\\n` +\n `built-in static \\`<HomeFallback>\\` component shipped in the\\n` +\n `template.\\n\\n` +\n `To add a CMS later:\\n\\n` +\n `1. Install Strapi or Propeller CMS in this folder.\\n` +\n `2. Install the matching adapter package in \\`../frontend/\\`:\\n` +\n ` - \\`npm i propeller-v2-cms-adapter-strapi\\`\\n` +\n `3. Wire it in \\`../frontend/app/providers.tsx\\` (Next) or\\n` +\n ` \\`src/main.ts\\` (Vue): pass the adapter into\\n` +\n ` \\`<CmsAdapterProvider>\\` / \\`provideCmsAdapter()\\`.\\n` +\n `4. Set \\`CMS_URL\\` in \\`../frontend/.env.local\\`.\\n` +\n `5. Update \\`propeller.json\\` → \\`cms.adapter\\` so \\`propeller doctor\\`\\n` +\n ` knows which adapter to verify.\\n`\n );\n }\n}\n"],"mappings":"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","names":["chalk","fs","path","execa","fs","path","fs","fs","execa","chalk"]}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
PropellerJsonSchema,
|
|
4
|
+
getCliVersion
|
|
5
|
+
} from "../chunk-UMI3HB67.js";
|
|
6
|
+
|
|
7
|
+
// src/bin/propeller.ts
|
|
8
|
+
import { Command } from "commander";
|
|
9
|
+
import chalk2 from "chalk";
|
|
10
|
+
|
|
11
|
+
// src/commands/doctor.ts
|
|
12
|
+
import { promises as fs } from "fs";
|
|
13
|
+
import * as path from "path";
|
|
14
|
+
import chalk from "chalk";
|
|
15
|
+
async function runDoctor(opts) {
|
|
16
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
17
|
+
const findings = [];
|
|
18
|
+
const manifestPath = await locateManifest(cwd);
|
|
19
|
+
if (!manifestPath) {
|
|
20
|
+
findings.push({
|
|
21
|
+
level: "fail",
|
|
22
|
+
message: "propeller.json not found in current dir or ./frontend/. Run from inside a scaffolded shop."
|
|
23
|
+
});
|
|
24
|
+
return report(findings);
|
|
25
|
+
}
|
|
26
|
+
const frontend = path.dirname(manifestPath);
|
|
27
|
+
let manifest;
|
|
28
|
+
try {
|
|
29
|
+
const raw = await fs.readFile(manifestPath, "utf8");
|
|
30
|
+
manifest = PropellerJsonSchema.parse(JSON.parse(raw));
|
|
31
|
+
findings.push({ level: "ok", message: `propeller.json valid (v1, ${manifest.shop.name}, ${manifest.shop.mode}).` });
|
|
32
|
+
} catch (err) {
|
|
33
|
+
findings.push({ level: "fail", message: `propeller.json invalid: ${err.message}` });
|
|
34
|
+
return report(findings);
|
|
35
|
+
}
|
|
36
|
+
const pkgPath = path.join(frontend, "package.json");
|
|
37
|
+
if (await pathExists(pkgPath)) {
|
|
38
|
+
const pkg = JSON.parse(await fs.readFile(pkgPath, "utf8"));
|
|
39
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
40
|
+
for (const name of Object.keys(deps).filter((d) => d.startsWith("propeller-v2-"))) {
|
|
41
|
+
const installed = path.join(frontend, "node_modules", name);
|
|
42
|
+
if (await pathExists(installed)) {
|
|
43
|
+
findings.push({ level: "ok", message: `${name} installed.` });
|
|
44
|
+
} else {
|
|
45
|
+
findings.push({ level: "warn", message: `${name} declared but not installed \u2014 run \`npm install\`.` });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
} else {
|
|
49
|
+
findings.push({ level: "warn", message: "frontend/package.json missing." });
|
|
50
|
+
}
|
|
51
|
+
await checkPortalMode(frontend, manifest.shop.portalMode, findings);
|
|
52
|
+
await checkB2BRoutesShape(frontend, manifest, findings);
|
|
53
|
+
await checkCmsAdapter(frontend, manifest, findings);
|
|
54
|
+
return report(findings);
|
|
55
|
+
}
|
|
56
|
+
async function locateManifest(cwd) {
|
|
57
|
+
const candidates = [
|
|
58
|
+
path.join(cwd, "propeller.json"),
|
|
59
|
+
path.join(cwd, "frontend", "propeller.json")
|
|
60
|
+
];
|
|
61
|
+
for (const c of candidates) {
|
|
62
|
+
if (await pathExists(c)) return c;
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
async function checkPortalMode(frontend, expected, findings) {
|
|
67
|
+
const candidates = [
|
|
68
|
+
path.join(frontend, "data", "config.ts"),
|
|
69
|
+
path.join(frontend, "src", "lib", "config.ts"),
|
|
70
|
+
path.join(frontend, "app", "utils", "config.ts")
|
|
71
|
+
];
|
|
72
|
+
for (const c of candidates) {
|
|
73
|
+
if (await pathExists(c)) {
|
|
74
|
+
const body = await fs.readFile(c, "utf8");
|
|
75
|
+
const hasMode = body.includes("portalMode") || body.includes("PORTAL_MODE");
|
|
76
|
+
if (!hasMode) {
|
|
77
|
+
findings.push({ level: "warn", message: `${path.relative(frontend, c)} has no portalMode reference.` });
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if (body.includes(`'${expected}'`)) {
|
|
81
|
+
findings.push({ level: "ok", message: `portalMode '${expected}' matches ${path.relative(frontend, c)}.` });
|
|
82
|
+
} else {
|
|
83
|
+
findings.push({
|
|
84
|
+
level: "fail",
|
|
85
|
+
message: `propeller.json declares portalMode '${expected}' but ${path.relative(frontend, c)} does not contain that literal.`
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
findings.push({ level: "warn", message: "No data/config.ts or src/lib/config.ts found \u2014 skipping portalMode check." });
|
|
92
|
+
}
|
|
93
|
+
async function checkB2BRoutesShape(frontend, manifest, findings) {
|
|
94
|
+
const b2bDirs = manifest.template.stack === "next" ? [
|
|
95
|
+
"app/account/quotes",
|
|
96
|
+
"app/account/authorization-requests",
|
|
97
|
+
"app/account/price-requests"
|
|
98
|
+
] : manifest.template.stack === "nuxt" ? [
|
|
99
|
+
"app/pages/account/quotes",
|
|
100
|
+
"app/pages/account/authorization-requests.vue",
|
|
101
|
+
"app/pages/account/price-requests.vue"
|
|
102
|
+
] : [
|
|
103
|
+
"src/views/account/QuotesView.vue",
|
|
104
|
+
"src/views/account/AuthorizationRequestsView.vue",
|
|
105
|
+
"src/views/account/PriceRequestsView.vue"
|
|
106
|
+
];
|
|
107
|
+
const shouldHaveB2B = manifest.shop.mode !== "b2c";
|
|
108
|
+
for (const rel of b2bDirs) {
|
|
109
|
+
const full = path.join(frontend, rel);
|
|
110
|
+
const present = await pathExists(full);
|
|
111
|
+
if (shouldHaveB2B && !present) {
|
|
112
|
+
findings.push({ level: "fail", message: `${rel} missing (mode=${manifest.shop.mode} expects B2B routes).` });
|
|
113
|
+
} else if (!shouldHaveB2B && present) {
|
|
114
|
+
findings.push({
|
|
115
|
+
level: "warn",
|
|
116
|
+
message: `${rel} present despite mode=b2c \u2014 should not have been scaffolded.`
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (findings.every((f) => !f.message.includes("B2B routes"))) {
|
|
121
|
+
findings.push({
|
|
122
|
+
level: "ok",
|
|
123
|
+
message: `B2B route presence matches mode=${manifest.shop.mode}.`
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
async function checkCmsAdapter(frontend, manifest, findings) {
|
|
128
|
+
if (manifest.cms.adapter === null) {
|
|
129
|
+
findings.push({ level: "ok", message: "No CMS adapter declared \u2014 frontend falls back to static homepage." });
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
const adapterPkg = `propeller-v2-cms-adapter-${manifest.cms.adapter}`;
|
|
133
|
+
const installed = path.join(frontend, "node_modules", adapterPkg);
|
|
134
|
+
if (await pathExists(installed)) {
|
|
135
|
+
findings.push({ level: "ok", message: `${adapterPkg} installed.` });
|
|
136
|
+
} else {
|
|
137
|
+
findings.push({
|
|
138
|
+
level: "warn",
|
|
139
|
+
message: `propeller.json declares cms.adapter='${manifest.cms.adapter}' but ${adapterPkg} is not installed \u2014 install it manually.`
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
function report(findings) {
|
|
144
|
+
let failed = 0;
|
|
145
|
+
for (const f of findings) {
|
|
146
|
+
const icon = f.level === "ok" ? chalk.green("\u2713") : f.level === "warn" ? chalk.yellow("!") : chalk.red("\u2717");
|
|
147
|
+
console.log(`${icon} ${f.message}`);
|
|
148
|
+
if (f.level === "fail") failed += 1;
|
|
149
|
+
}
|
|
150
|
+
console.log("");
|
|
151
|
+
if (failed === 0) {
|
|
152
|
+
console.log(chalk.green("All checks passed."));
|
|
153
|
+
} else {
|
|
154
|
+
console.log(chalk.red(`${failed} check(s) failed.`));
|
|
155
|
+
}
|
|
156
|
+
return failed === 0 ? 0 : 1;
|
|
157
|
+
}
|
|
158
|
+
async function pathExists(p) {
|
|
159
|
+
try {
|
|
160
|
+
await fs.access(p);
|
|
161
|
+
return true;
|
|
162
|
+
} catch {
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// src/bin/propeller.ts
|
|
168
|
+
async function main() {
|
|
169
|
+
const cli = new Command();
|
|
170
|
+
const version = await getCliVersion();
|
|
171
|
+
cli.name("propeller").description("Post-scaffold tooling for Propeller Commerce shops.").version(version);
|
|
172
|
+
cli.command("doctor").description("Sanity-check the current shop (validates propeller.json, version pins, route shape).").option("--cwd <dir>", "Run as if invoked from <dir>").action(async (options) => {
|
|
173
|
+
try {
|
|
174
|
+
const code = await runDoctor({ cwd: options.cwd });
|
|
175
|
+
process.exit(code);
|
|
176
|
+
} catch (err) {
|
|
177
|
+
console.error(chalk2.red(`Doctor failed: ${err.message}`));
|
|
178
|
+
process.exit(1);
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
cli.command("upgrade").description("(Phase B, not yet implemented) Apply template diffs to this shop.").action(() => {
|
|
182
|
+
console.log(
|
|
183
|
+
chalk2.yellow(
|
|
184
|
+
"propeller upgrade is reserved for v0.2. The propeller.json schema records the template version so the upgrade path is non-breaking once shipped."
|
|
185
|
+
)
|
|
186
|
+
);
|
|
187
|
+
process.exit(2);
|
|
188
|
+
});
|
|
189
|
+
cli.command("eject").description("(Phase B, not yet implemented) Mark a file as customised.").argument("<path>").action(() => {
|
|
190
|
+
console.log(
|
|
191
|
+
chalk2.yellow(
|
|
192
|
+
"propeller eject is reserved for v0.2. v0.1 ships the customisations.ejected list in propeller.json so manual entries are forward-compatible."
|
|
193
|
+
)
|
|
194
|
+
);
|
|
195
|
+
process.exit(2);
|
|
196
|
+
});
|
|
197
|
+
await cli.parseAsync(process.argv);
|
|
198
|
+
}
|
|
199
|
+
main().catch((err) => {
|
|
200
|
+
console.error(chalk2.red(`Unexpected error: ${err.message}`));
|
|
201
|
+
process.exit(1);
|
|
202
|
+
});
|
|
203
|
+
//# sourceMappingURL=propeller.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/bin/propeller.ts","../../src/commands/doctor.ts"],"sourcesContent":["/**\n * `propeller` — post-scaffold ops for a Propeller shop.\n *\n * Sub-commands:\n * propeller doctor sanity-check the current shop\n *\n * Phase B (deferred):\n * propeller upgrade [--to X.Y.Z]\n * propeller eject <path>\n *\n * Phase C (deferred):\n * propeller migrate-mode <new-mode>\n */\n\nimport { Command } from 'commander';\nimport chalk from 'chalk';\nimport { runDoctor } from '../commands/doctor';\nimport { getCliVersion } from '../util/version';\n\nasync function main(): Promise<void> {\n const cli = new Command();\n const version = await getCliVersion();\n\n cli\n .name('propeller')\n .description('Post-scaffold tooling for Propeller Commerce shops.')\n .version(version);\n\n cli\n .command('doctor')\n .description('Sanity-check the current shop (validates propeller.json, version pins, route shape).')\n .option('--cwd <dir>', 'Run as if invoked from <dir>')\n .action(async (options: { cwd?: string }) => {\n try {\n const code = await runDoctor({ cwd: options.cwd });\n process.exit(code);\n } catch (err) {\n // eslint-disable-next-line no-console\n console.error(chalk.red(`Doctor failed: ${(err as Error).message}`));\n process.exit(1);\n }\n });\n\n cli\n .command('upgrade')\n .description('(Phase B, not yet implemented) Apply template diffs to this shop.')\n .action(() => {\n // eslint-disable-next-line no-console\n console.log(\n chalk.yellow(\n 'propeller upgrade is reserved for v0.2. The propeller.json schema records the template version so the upgrade path is non-breaking once shipped.'\n )\n );\n process.exit(2);\n });\n\n cli\n .command('eject')\n .description('(Phase B, not yet implemented) Mark a file as customised.')\n .argument('<path>')\n .action(() => {\n // eslint-disable-next-line no-console\n console.log(\n chalk.yellow(\n 'propeller eject is reserved for v0.2. v0.1 ships the customisations.ejected list in propeller.json so manual entries are forward-compatible.'\n )\n );\n process.exit(2);\n });\n\n await cli.parseAsync(process.argv);\n}\n\nmain().catch((err) => {\n // eslint-disable-next-line no-console\n console.error(chalk.red(`Unexpected error: ${(err as Error).message}`));\n process.exit(1);\n});\n","/**\n * `propeller doctor` — sanity-check the current shop.\n *\n * Reads `frontend/propeller.json` (or the file at the given\n * path) and verifies the shop matches what the manifest declares.\n *\n * v0.1 checks:\n * - propeller.json validates against the Zod schema\n * - All `propeller-v2-*` packages declared in package.json resolve\n * against installed copies (no missing deps)\n * - data/config.ts (or src/lib/config.ts) contains the declared\n * portalMode literal (grep — not a full parse)\n * - B2B routes exist iff mode !== 'b2c'\n *\n * Exit 0 = all green, 1 = at least one red. Yellow checks log warnings\n * but do not fail.\n */\n\nimport { promises as fs } from 'node:fs';\nimport * as path from 'node:path';\nimport chalk from 'chalk';\nimport { PropellerJsonSchema, type PropellerJson } from '../schema/propellerJson';\n\nexport interface DoctorOptions {\n cwd?: string;\n}\n\ntype Finding = { level: 'ok' | 'warn' | 'fail'; message: string };\n\nexport async function runDoctor(opts: DoctorOptions): Promise<number> {\n const cwd = opts.cwd ?? process.cwd();\n const findings: Finding[] = [];\n\n // Resolve the propeller.json — try ./propeller.json then ./frontend/propeller.json.\n const manifestPath = await locateManifest(cwd);\n if (!manifestPath) {\n findings.push({\n level: 'fail',\n message:\n 'propeller.json not found in current dir or ./frontend/. Run from inside a scaffolded shop.',\n });\n return report(findings);\n }\n const frontend = path.dirname(manifestPath);\n\n // 1. Schema check.\n let manifest: PropellerJson;\n try {\n const raw = await fs.readFile(manifestPath, 'utf8');\n manifest = PropellerJsonSchema.parse(JSON.parse(raw));\n findings.push({ level: 'ok', message: `propeller.json valid (v1, ${manifest.shop.name}, ${manifest.shop.mode}).` });\n } catch (err) {\n findings.push({ level: 'fail', message: `propeller.json invalid: ${(err as Error).message}` });\n return report(findings);\n }\n\n // 2. Package presence.\n const pkgPath = path.join(frontend, 'package.json');\n if (await pathExists(pkgPath)) {\n const pkg = JSON.parse(await fs.readFile(pkgPath, 'utf8')) as {\n dependencies?: Record<string, string>;\n devDependencies?: Record<string, string>;\n };\n const deps = { ...pkg.dependencies, ...pkg.devDependencies };\n for (const name of Object.keys(deps).filter((d) => d.startsWith('propeller-v2-'))) {\n const installed = path.join(frontend, 'node_modules', name);\n if (await pathExists(installed)) {\n findings.push({ level: 'ok', message: `${name} installed.` });\n } else {\n findings.push({ level: 'warn', message: `${name} declared but not installed — run \\`npm install\\`.` });\n }\n }\n } else {\n findings.push({ level: 'warn', message: 'frontend/package.json missing.' });\n }\n\n // 3. Portal mode literal in config.\n await checkPortalMode(frontend, manifest.shop.portalMode, findings);\n\n // 4. B2B routes presence vs mode.\n await checkB2BRoutesShape(frontend, manifest, findings);\n\n // 5. CMS adapter wiring.\n await checkCmsAdapter(frontend, manifest, findings);\n\n return report(findings);\n}\n\nasync function locateManifest(cwd: string): Promise<string | null> {\n const candidates = [\n path.join(cwd, 'propeller.json'),\n path.join(cwd, 'frontend', 'propeller.json'),\n ];\n for (const c of candidates) {\n if (await pathExists(c)) return c;\n }\n return null;\n}\n\nasync function checkPortalMode(\n frontend: string,\n expected: string,\n findings: Finding[]\n): Promise<void> {\n const candidates = [\n path.join(frontend, 'data', 'config.ts'),\n path.join(frontend, 'src', 'lib', 'config.ts'),\n path.join(frontend, 'app', 'utils', 'config.ts'),\n ];\n for (const c of candidates) {\n if (await pathExists(c)) {\n const body = await fs.readFile(c, 'utf8');\n // Find any line that mentions portalMode, then check the expected literal nearby.\n const hasMode = body.includes('portalMode') || body.includes('PORTAL_MODE');\n if (!hasMode) {\n findings.push({ level: 'warn', message: `${path.relative(frontend, c)} has no portalMode reference.` });\n return;\n }\n if (body.includes(`'${expected}'`)) {\n findings.push({ level: 'ok', message: `portalMode '${expected}' matches ${path.relative(frontend, c)}.` });\n } else {\n findings.push({\n level: 'fail',\n message: `propeller.json declares portalMode '${expected}' but ${path.relative(frontend, c)} does not contain that literal.`,\n });\n }\n return;\n }\n }\n findings.push({ level: 'warn', message: 'No data/config.ts or src/lib/config.ts found — skipping portalMode check.' });\n}\n\nasync function checkB2BRoutesShape(\n frontend: string,\n manifest: PropellerJson,\n findings: Finding[]\n): Promise<void> {\n const b2bDirs =\n manifest.template.stack === 'next'\n ? [\n 'app/account/quotes',\n 'app/account/authorization-requests',\n 'app/account/price-requests',\n ]\n : manifest.template.stack === 'nuxt'\n ? [\n 'app/pages/account/quotes',\n 'app/pages/account/authorization-requests.vue',\n 'app/pages/account/price-requests.vue',\n ]\n : [\n 'src/views/account/QuotesView.vue',\n 'src/views/account/AuthorizationRequestsView.vue',\n 'src/views/account/PriceRequestsView.vue',\n ];\n const shouldHaveB2B = manifest.shop.mode !== 'b2c';\n for (const rel of b2bDirs) {\n const full = path.join(frontend, rel);\n const present = await pathExists(full);\n if (shouldHaveB2B && !present) {\n findings.push({ level: 'fail', message: `${rel} missing (mode=${manifest.shop.mode} expects B2B routes).` });\n } else if (!shouldHaveB2B && present) {\n findings.push({\n level: 'warn',\n message: `${rel} present despite mode=b2c — should not have been scaffolded.`,\n });\n }\n }\n if (findings.every((f) => !f.message.includes('B2B routes'))) {\n findings.push({\n level: 'ok',\n message: `B2B route presence matches mode=${manifest.shop.mode}.`,\n });\n }\n}\n\nasync function checkCmsAdapter(\n frontend: string,\n manifest: PropellerJson,\n findings: Finding[]\n): Promise<void> {\n if (manifest.cms.adapter === null) {\n findings.push({ level: 'ok', message: 'No CMS adapter declared — frontend falls back to static homepage.' });\n return;\n }\n const adapterPkg = `propeller-v2-cms-adapter-${manifest.cms.adapter}`;\n const installed = path.join(frontend, 'node_modules', adapterPkg);\n if (await pathExists(installed)) {\n findings.push({ level: 'ok', message: `${adapterPkg} installed.` });\n } else {\n // Warn (not fail): the adapter packages are not yet published to npm and\n // are not pinned in `package.json` at scaffold time. The user installs\n // them manually from the accelerator monorepo. doctor flags the gap so\n // the omission isn't silent, but doesn't block the shop.\n findings.push({\n level: 'warn',\n message: `propeller.json declares cms.adapter='${manifest.cms.adapter}' but ${adapterPkg} is not installed — install it manually.`,\n });\n }\n}\n\nfunction report(findings: Finding[]): number {\n let failed = 0;\n for (const f of findings) {\n const icon =\n f.level === 'ok'\n ? chalk.green('✓')\n : f.level === 'warn'\n ? chalk.yellow('!')\n : chalk.red('✗');\n // eslint-disable-next-line no-console\n console.log(`${icon} ${f.message}`);\n if (f.level === 'fail') failed += 1;\n }\n // eslint-disable-next-line no-console\n console.log('');\n if (failed === 0) {\n // eslint-disable-next-line no-console\n console.log(chalk.green('All checks passed.'));\n } else {\n // eslint-disable-next-line no-console\n console.log(chalk.red(`${failed} check(s) failed.`));\n }\n return failed === 0 ? 0 : 1;\n}\n\nasync function pathExists(p: string): Promise<boolean> {\n try {\n await fs.access(p);\n return true;\n } catch {\n return false;\n }\n}\n"],"mappings":"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","names":["chalk","chalk"]}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/util/version.ts
|
|
4
|
+
import { promises as fs } from "fs";
|
|
5
|
+
import * as path from "path";
|
|
6
|
+
import { fileURLToPath } from "url";
|
|
7
|
+
var cached = null;
|
|
8
|
+
async function getCliVersion() {
|
|
9
|
+
if (cached) return cached;
|
|
10
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
for (let depth = 0; depth < 6; depth += 1) {
|
|
12
|
+
const candidate = path.resolve(here, ...Array(depth + 1).fill(".."), "package.json");
|
|
13
|
+
try {
|
|
14
|
+
const raw = await fs.readFile(candidate, "utf8");
|
|
15
|
+
const pkg = JSON.parse(raw);
|
|
16
|
+
if (pkg.name === "create-propeller-shop" && pkg.version) {
|
|
17
|
+
cached = pkg.version;
|
|
18
|
+
return cached;
|
|
19
|
+
}
|
|
20
|
+
} catch {
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
cached = "0.0.0";
|
|
24
|
+
return cached;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// src/schema/propellerJson.ts
|
|
28
|
+
import { z } from "zod";
|
|
29
|
+
var ShopMode = z.enum(["b2b", "b2c", "hybrid"]);
|
|
30
|
+
var PortalMode = z.enum(["open", "semi-closed", "closed"]);
|
|
31
|
+
var CmsAdapter = z.enum(["strapi", "cms"]).nullable();
|
|
32
|
+
var Stack = z.enum(["next", "vue", "nuxt"]);
|
|
33
|
+
var PropellerJsonSchema = z.object({
|
|
34
|
+
$schema: z.string().optional(),
|
|
35
|
+
template: z.object({
|
|
36
|
+
/** Template package name, e.g. "propeller-shop-template-next". */
|
|
37
|
+
name: z.string(),
|
|
38
|
+
/** Semver of the template at scaffold time. Used by `propeller upgrade`. */
|
|
39
|
+
version: z.string(),
|
|
40
|
+
stack: Stack
|
|
41
|
+
}),
|
|
42
|
+
shop: z.object({
|
|
43
|
+
/** Human-readable shop name, used in OG titles, package.json, README. */
|
|
44
|
+
name: z.string(),
|
|
45
|
+
mode: ShopMode,
|
|
46
|
+
/** BCP-47 locale codes the shop supports. Must contain `defaultLocale`. */
|
|
47
|
+
locales: z.array(z.string()).min(1),
|
|
48
|
+
defaultLocale: z.string(),
|
|
49
|
+
/** Display symbol shown to humans, e.g. '€'. */
|
|
50
|
+
currency: z.string(),
|
|
51
|
+
/** ISO 4217 code parsed by crawlers, e.g. 'EUR'. */
|
|
52
|
+
currencyCode: z.string().length(3),
|
|
53
|
+
portalMode: PortalMode,
|
|
54
|
+
/** Public site origin (no trailing slash), used for SEO and canonical URLs. */
|
|
55
|
+
siteUrl: z.string().url()
|
|
56
|
+
}),
|
|
57
|
+
/**
|
|
58
|
+
* Feature flags for whole route trees. `quotes: false` means the upgrade
|
|
59
|
+
* tool skips diffs for `app/account/quotes/*` and the build can tree-shake.
|
|
60
|
+
* For mode=b2c, B2B-only flags are false; mode=hybrid leaves them true and
|
|
61
|
+
* gates at runtime via userMode.
|
|
62
|
+
*/
|
|
63
|
+
features: z.object({
|
|
64
|
+
quotes: z.boolean(),
|
|
65
|
+
authorization: z.boolean(),
|
|
66
|
+
favorites: z.boolean(),
|
|
67
|
+
clusters: z.boolean(),
|
|
68
|
+
search: z.boolean(),
|
|
69
|
+
contacts: z.boolean()
|
|
70
|
+
}),
|
|
71
|
+
cms: z.object({
|
|
72
|
+
adapter: CmsAdapter,
|
|
73
|
+
/** Typically "${CMS_URL}" — resolved by the runtime, not the CLI. */
|
|
74
|
+
endpoint: z.string(),
|
|
75
|
+
preview: z.boolean()
|
|
76
|
+
}),
|
|
77
|
+
customisations: z.object({
|
|
78
|
+
/**
|
|
79
|
+
* Paths (relative to `frontend/`) marked as customised.
|
|
80
|
+
* `propeller upgrade` skips files in this list. v0.1 reserves the field
|
|
81
|
+
* but doesn't actively read it yet — Phase B does.
|
|
82
|
+
*/
|
|
83
|
+
ejected: z.array(z.string()).default([])
|
|
84
|
+
})
|
|
85
|
+
});
|
|
86
|
+
function buildPropellerJson(input) {
|
|
87
|
+
const isB2C = input.mode === "b2c";
|
|
88
|
+
return {
|
|
89
|
+
$schema: "https://propeller.dev/schemas/shop.v1.json",
|
|
90
|
+
template: {
|
|
91
|
+
name: input.templateName,
|
|
92
|
+
version: input.templateVersion,
|
|
93
|
+
stack: input.stack
|
|
94
|
+
},
|
|
95
|
+
shop: {
|
|
96
|
+
name: input.shopName,
|
|
97
|
+
mode: input.mode,
|
|
98
|
+
locales: input.locales,
|
|
99
|
+
defaultLocale: input.defaultLocale,
|
|
100
|
+
currency: input.currency,
|
|
101
|
+
currencyCode: input.currencyCode,
|
|
102
|
+
portalMode: input.portalMode,
|
|
103
|
+
siteUrl: input.siteUrl
|
|
104
|
+
},
|
|
105
|
+
features: {
|
|
106
|
+
quotes: !isB2C,
|
|
107
|
+
authorization: !isB2C,
|
|
108
|
+
favorites: true,
|
|
109
|
+
clusters: true,
|
|
110
|
+
search: true,
|
|
111
|
+
contacts: !isB2C
|
|
112
|
+
},
|
|
113
|
+
cms: {
|
|
114
|
+
adapter: input.cmsAdapter,
|
|
115
|
+
endpoint: "${CMS_URL}",
|
|
116
|
+
preview: true
|
|
117
|
+
},
|
|
118
|
+
customisations: {
|
|
119
|
+
ejected: []
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export {
|
|
125
|
+
PropellerJsonSchema,
|
|
126
|
+
buildPropellerJson,
|
|
127
|
+
getCliVersion
|
|
128
|
+
};
|
|
129
|
+
//# sourceMappingURL=chunk-UMI3HB67.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/util/version.ts","../src/schema/propellerJson.ts"],"sourcesContent":["/**\n * Read the CLI's own version from its bundled package.json.\n * Used as the `template.version` value in the scaffolded `propeller.json`.\n */\n\nimport { promises as fs } from 'node:fs';\nimport * as path from 'node:path';\nimport { fileURLToPath } from 'node:url';\n\nlet cached: string | null = null;\n\nexport async function getCliVersion(): Promise<string> {\n if (cached) return cached;\n const here = path.dirname(fileURLToPath(import.meta.url));\n // The compiled bin lives at dist/bin/foo.js. Walk up from `here` until we\n // find a package.json whose name matches our CLI — works for both the\n // bin layout (dist/bin/foo.js) and the helper layout (dist/util/foo.js).\n for (let depth = 0; depth < 6; depth += 1) {\n const candidate = path.resolve(here, ...Array(depth + 1).fill('..'), 'package.json');\n try {\n const raw = await fs.readFile(candidate, 'utf8');\n const pkg = JSON.parse(raw) as { name?: string; version?: string };\n if (pkg.name === 'create-propeller-shop' && pkg.version) {\n cached = pkg.version;\n return cached;\n }\n } catch {\n // Try next depth.\n }\n }\n // Fallback so doctor still works in development checkouts.\n cached = '0.0.0';\n return cached;\n}\n","/**\n * propeller.json — the scaffolded shop's identity card.\n *\n * Lives at `frontend/propeller.json` in every scaffolded shop.\n * Owned by the shop. Edited rarely. Read by:\n *\n * - The CLI's `propeller doctor` (verify pinned versions + config drift).\n * - The CLI's future `propeller upgrade` (Phase B; reads template version\n * and customisations.ejected to decide what to apply).\n * - The shop's own runtime (`data/config.ts` reads shop.mode etc. at boot\n * to populate the PropellerProvider).\n *\n * Schema versioning: this is v1. Breaking changes bump `$schema` and the\n * CLI handles migration. v0.1 ships v1 only.\n */\n\nimport { z } from 'zod';\n\nexport const ShopMode = z.enum(['b2b', 'b2c', 'hybrid']);\nexport const PortalMode = z.enum(['open', 'semi-closed', 'closed']);\nexport const CmsAdapter = z.enum(['strapi', 'cms']).nullable();\nexport const Stack = z.enum(['next', 'vue', 'nuxt']);\n\nexport const PropellerJsonSchema = z.object({\n $schema: z.string().optional(),\n template: z.object({\n /** Template package name, e.g. \"propeller-shop-template-next\". */\n name: z.string(),\n /** Semver of the template at scaffold time. Used by `propeller upgrade`. */\n version: z.string(),\n stack: Stack,\n }),\n shop: z.object({\n /** Human-readable shop name, used in OG titles, package.json, README. */\n name: z.string(),\n mode: ShopMode,\n /** BCP-47 locale codes the shop supports. Must contain `defaultLocale`. */\n locales: z.array(z.string()).min(1),\n defaultLocale: z.string(),\n /** Display symbol shown to humans, e.g. '€'. */\n currency: z.string(),\n /** ISO 4217 code parsed by crawlers, e.g. 'EUR'. */\n currencyCode: z.string().length(3),\n portalMode: PortalMode,\n /** Public site origin (no trailing slash), used for SEO and canonical URLs. */\n siteUrl: z.string().url(),\n }),\n /**\n * Feature flags for whole route trees. `quotes: false` means the upgrade\n * tool skips diffs for `app/account/quotes/*` and the build can tree-shake.\n * For mode=b2c, B2B-only flags are false; mode=hybrid leaves them true and\n * gates at runtime via userMode.\n */\n features: z.object({\n quotes: z.boolean(),\n authorization: z.boolean(),\n favorites: z.boolean(),\n clusters: z.boolean(),\n search: z.boolean(),\n contacts: z.boolean(),\n }),\n cms: z.object({\n adapter: CmsAdapter,\n /** Typically \"${CMS_URL}\" — resolved by the runtime, not the CLI. */\n endpoint: z.string(),\n preview: z.boolean(),\n }),\n customisations: z.object({\n /**\n * Paths (relative to `frontend/`) marked as customised.\n * `propeller upgrade` skips files in this list. v0.1 reserves the field\n * but doesn't actively read it yet — Phase B does.\n */\n ejected: z.array(z.string()).default([]),\n }),\n});\n\nexport type PropellerJson = z.infer<typeof PropellerJsonSchema>;\nexport type ShopMode = z.infer<typeof ShopMode>;\nexport type PortalMode = z.infer<typeof PortalMode>;\nexport type CmsAdapter = z.infer<typeof CmsAdapter>;\nexport type Stack = z.infer<typeof Stack>;\n\n/**\n * Sensible defaults that turn a `ShopConfig` (from CLI prompts) into a\n * complete `PropellerJson`. The CLI fills in `template.version` from the\n * CLI's own package.json at scaffold time.\n */\nexport function buildPropellerJson(input: {\n templateName: string;\n templateVersion: string;\n stack: Stack;\n shopName: string;\n mode: ShopMode;\n locales: string[];\n defaultLocale: string;\n currency: string;\n currencyCode: string;\n portalMode: PortalMode;\n siteUrl: string;\n cmsAdapter: CmsAdapter;\n}): PropellerJson {\n const isB2C = input.mode === 'b2c';\n return {\n $schema: 'https://propeller.dev/schemas/shop.v1.json',\n template: {\n name: input.templateName,\n version: input.templateVersion,\n stack: input.stack,\n },\n shop: {\n name: input.shopName,\n mode: input.mode,\n locales: input.locales,\n defaultLocale: input.defaultLocale,\n currency: input.currency,\n currencyCode: input.currencyCode,\n portalMode: input.portalMode,\n siteUrl: input.siteUrl,\n },\n features: {\n quotes: !isB2C,\n authorization: !isB2C,\n favorites: true,\n clusters: true,\n search: true,\n contacts: !isB2C,\n },\n cms: {\n adapter: input.cmsAdapter,\n endpoint: '${CMS_URL}',\n preview: true,\n },\n customisations: {\n ejected: [],\n },\n };\n}\n"],"mappings":"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","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@propeller-commerce/create-propeller-shop",
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"publishConfig": {
|
|
5
|
+
"access": "public"
|
|
6
|
+
},
|
|
7
|
+
"description": "Scaffold a new Propeller Commerce shop in minutes. Templates + prompts + version pinning.",
|
|
8
|
+
"license": "MIT",
|
|
9
|
+
"type": "module",
|
|
10
|
+
"main": "./dist/index.js",
|
|
11
|
+
"bin": {
|
|
12
|
+
"create-propeller-shop": "./dist/bin/create-propeller-shop.js",
|
|
13
|
+
"propeller": "./dist/bin/propeller.js"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"templates",
|
|
18
|
+
"README.md",
|
|
19
|
+
"LICENSE"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsup",
|
|
23
|
+
"build:templates": "node scripts/copy-templates.mjs",
|
|
24
|
+
"dev": "tsup --watch",
|
|
25
|
+
"typecheck": "tsc --noEmit",
|
|
26
|
+
"clean": "powershell -Command \"Remove-Item -Recurse -Force dist,templates -ErrorAction SilentlyContinue\"",
|
|
27
|
+
"prepublishOnly": "npm run build:templates && npm run build"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@inquirer/prompts": "^7.0.0",
|
|
31
|
+
"chalk": "^5.3.0",
|
|
32
|
+
"commander": "^12.1.0",
|
|
33
|
+
"execa": "^9.5.0",
|
|
34
|
+
"fs-extra": "^11.2.0",
|
|
35
|
+
"handlebars": "^4.7.8",
|
|
36
|
+
"ora": "^8.1.0",
|
|
37
|
+
"semver": "^7.6.3",
|
|
38
|
+
"zod": "^3.23.8"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@types/fs-extra": "^11.0.4",
|
|
42
|
+
"@types/node": "^20",
|
|
43
|
+
"@types/semver": "^7.5.8",
|
|
44
|
+
"tsup": "^8.3.5",
|
|
45
|
+
"typescript": "~5.9"
|
|
46
|
+
},
|
|
47
|
+
"engines": {
|
|
48
|
+
"node": ">=18"
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_comment": "Paths (relative to the scaffolded frontend/) to delete when mode === 'b2c'. The boilerplate ships every route by default; b2c shops trim the b2b-only ones.",
|
|
3
|
+
"remove": [
|
|
4
|
+
"app/account/authorization-requests",
|
|
5
|
+
"app/account/authorization-settings",
|
|
6
|
+
"app/account/price-requests",
|
|
7
|
+
"app/account/quote-requests",
|
|
8
|
+
"app/account/quotes",
|
|
9
|
+
"app/authorization-request-sent"
|
|
10
|
+
]
|
|
11
|
+
}
|