diff --git a/.changeset/spotty-planes-boil.md b/.changeset/spotty-planes-boil.md new file mode 100644 index 00000000..20b6449e --- /dev/null +++ b/.changeset/spotty-planes-boil.md @@ -0,0 +1,5 @@ +--- +'create-solana-dapp': minor +--- + +dynamic template lists by fetching them from registry url diff --git a/.github/workflows/e2e-test.yml b/.github/workflows/e2e-test.yml index aa28547d..0c7db853 100644 --- a/.github/workflows/e2e-test.yml +++ b/.github/workflows/e2e-test.yml @@ -21,24 +21,24 @@ jobs: matrix: node: [20, 22] pm: [npm, pnpm, yarn] - template: - [ - gill-next-tailwind, - gill-next-tailwind-basic, - gill-next-tailwind-counter, - gill-node-express, - gill-node-script, - gill-react-vite-tailwind, - gill-react-vite-tailwind-basic, - gill-react-vite-tailwind-counter, + template: [ + # TODO: Make these values dynamic by adding a parameter to CLI + template-next-tailwind, + template-next-tailwind-basic, + template-next-tailwind-counter, + template-node-express, + template-node-script, + template-react-vite-tailwind, + template-react-vite-tailwind-basic, + template-react-vite-tailwind-counter, web3js-expo, web3js-expo-paper, - web3js-next-tailwind, - web3js-next-tailwind-basic, - web3js-next-tailwind-counter, - web3js-react-vite-tailwind, - web3js-react-vite-tailwind-basic, - web3js-react-vite-tailwind-counter, + legacy-next-tailwind, + legacy-next-tailwind-basic, + legacy-next-tailwind-counter, + legacy-react-vite-tailwind, + legacy-react-vite-tailwind-basic, + legacy-react-vite-tailwind-counter, ] steps: diff --git a/package.json b/package.json index 31ea73c3..3c1fe528 100644 --- a/package.json +++ b/package.json @@ -67,6 +67,7 @@ }, "packageManager": "pnpm@10.5.2", "dependencies": { + "@beeman/repokit": "0.0.0-canary-20250801172233", "@clack/prompts": "^0.7.0", "commander": "^13.1.0", "giget": "^1.2.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 404bb16d..595603e3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: dependencies: + '@beeman/repokit': + specifier: 0.0.0-canary-20250801172233 + version: 0.0.0-canary-20250801172233 '@clack/prompts': specifier: ^0.7.0 version: 0.7.0 @@ -163,6 +166,10 @@ packages: resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} engines: {node: '>=18'} + '@beeman/repokit@0.0.0-canary-20250801172233': + resolution: {integrity: sha512-oNneWXwe6w6iZxyTbStlh4uEs34s8/BmYZTd8OOhLEoylhYfUJd1xAHz714fcn2Tyz+WRTJYO9qHI+2rQ4vIxw==} + hasBin: true + '@changesets/apply-release-plan@7.0.12': resolution: {integrity: sha512-EaET7As5CeuhTzvXTQCRZeBUcisoYPDDcXvgTE/2jmmypKp0RC7LxKj/yzqeh/1qFTZI7oDGFcL1PHRuQuketQ==} @@ -440,6 +447,14 @@ packages: resolution: {integrity: sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA==} engines: {node: '>=18.18'} + '@isaacs/balanced-match@4.0.1': + resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==} + engines: {node: 20 || >=22} + + '@isaacs/brace-expansion@5.0.0': + resolution: {integrity: sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==} + engines: {node: 20 || >=22} + '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} @@ -1425,6 +1440,10 @@ packages: resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==} engines: {node: '>=14'} + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} + fraction.js@4.3.7: resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} @@ -1472,6 +1491,11 @@ packages: resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} hasBin: true + glob@11.0.3: + resolution: {integrity: sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==} + engines: {node: 20 || >=22} + hasBin: true + global-directory@4.0.1: resolution: {integrity: sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==} engines: {node: '>=18'} @@ -1652,6 +1676,10 @@ packages: jackspeak@3.4.3: resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + jackspeak@4.1.1: + resolution: {integrity: sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==} + engines: {node: 20 || >=22} + jiti@1.21.7: resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==} hasBin: true @@ -1754,6 +1782,10 @@ packages: lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + lru-cache@11.1.0: + resolution: {integrity: sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A==} + engines: {node: 20 || >=22} + lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} @@ -1804,6 +1836,10 @@ packages: resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} engines: {node: '>=4'} + minimatch@10.0.3: + resolution: {integrity: sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==} + engines: {node: 20 || >=22} + minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} @@ -1989,6 +2025,10 @@ packages: resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} engines: {node: '>=16 || 14 >=14.18'} + path-scurry@2.0.0: + resolution: {integrity: sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==} + engines: {node: 20 || >=22} + path-type@4.0.0: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} @@ -2849,6 +2889,16 @@ snapshots: '@bcoe/v8-coverage@1.0.2': {} + '@beeman/repokit@0.0.0-canary-20250801172233': + dependencies: + '@clack/prompts': 0.7.0 + commander: 13.1.0 + giget: 1.2.4 + glob: 11.0.3 + picocolors: 1.1.1 + semver: 7.7.1 + zod: 3.24.1 + '@changesets/apply-release-plan@7.0.12': dependencies: '@changesets/config': 3.1.1 @@ -3151,6 +3201,12 @@ snapshots: '@humanwhocodes/retry@0.4.1': {} + '@isaacs/balanced-match@4.0.1': {} + + '@isaacs/brace-expansion@5.0.0': + dependencies: + '@isaacs/balanced-match': 4.0.1 + '@isaacs/cliui@8.0.2': dependencies: string-width: 5.1.2 @@ -4198,6 +4254,11 @@ snapshots: cross-spawn: 7.0.6 signal-exit: 4.1.0 + foreground-child@3.3.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + fraction.js@4.3.7: {} fs-extra@7.0.1: @@ -4253,6 +4314,15 @@ snapshots: package-json-from-dist: 1.0.1 path-scurry: 1.11.1 + glob@11.0.3: + dependencies: + foreground-child: 3.3.1 + jackspeak: 4.1.1 + minimatch: 10.0.3 + minipass: 7.1.2 + package-json-from-dist: 1.0.1 + path-scurry: 2.0.0 + global-directory@4.0.1: dependencies: ini: 4.1.1 @@ -4407,6 +4477,10 @@ snapshots: optionalDependencies: '@pkgjs/parseargs': 0.11.0 + jackspeak@4.1.1: + dependencies: + '@isaacs/cliui': 8.0.2 + jiti@1.21.7: {} jiti@2.4.2: {} @@ -4483,6 +4557,8 @@ snapshots: lru-cache@10.4.3: {} + lru-cache@11.1.0: {} + lru-cache@5.1.1: dependencies: yallist: 3.1.1 @@ -4546,6 +4622,10 @@ snapshots: min-indent@1.0.1: {} + minimatch@10.0.3: + dependencies: + '@isaacs/brace-expansion': 5.0.0 + minimatch@3.1.2: dependencies: brace-expansion: 1.1.11 @@ -4727,6 +4807,11 @@ snapshots: lru-cache: 10.4.3 minipass: 7.1.2 + path-scurry@2.0.0: + dependencies: + lru-cache: 11.1.0 + minipass: 7.1.2 + path-type@4.0.0: {} path-type@6.0.0: {} diff --git a/src/templates/frameworks.ts b/src/templates/frameworks.ts deleted file mode 100644 index 733d759d..00000000 --- a/src/templates/frameworks.ts +++ /dev/null @@ -1,119 +0,0 @@ -import { Template } from './templates' - -export interface Framework { - id: string - name: string - description: string - templates: Template[] -} - -export const frameworks: Framework[] = [ - { - id: 'next', - name: 'Next.js', - description: 'A React framework by Vercel', - templates: [ - { - name: 'gill-next-tailwind', - description: 'Next.js, Tailwind, gill (based on @solana/kit), Wallet UI', - repository: 'gh:solana-foundation/templates/templates/template-next-tailwind', - }, - { - name: 'gill-next-tailwind-basic', - description: 'Next.js, Tailwind, basic Anchor example, gill (based on @solana/kit), Wallet UI', - repository: 'gh:solana-foundation/templates/templates/template-next-tailwind-basic', - }, - { - name: 'gill-next-tailwind-counter', - description: 'Next.js, Tailwind, Anchor Counter Example, gill (based on @solana/kit), Wallet UI', - repository: 'gh:solana-foundation/templates/templates/template-next-tailwind-counter', - }, - { - name: 'web3js-next-tailwind', - description: 'Legacy Next.js, Tailwind, no Anchor, @solana/web3.js, Wallet Adapter', - repository: 'gh:solana-foundation/templates/legacy/legacy-next-tailwind', - }, - { - name: 'web3js-next-tailwind-basic', - description: 'Legacy Next.js, Tailwind, basic Anchor example, @solana/web3.js, Wallet Adapter', - repository: 'gh:solana-foundation/templates/legacy/legacy-next-tailwind-basic', - }, - { - name: 'web3js-next-tailwind-counter', - description: 'Next.js, Tailwind, Anchor Counter Example, @solana/web3.js, Wallet Adapter', - repository: 'gh:solana-foundation/templates/legacy/legacy-next-tailwind-counter', - }, - ], - }, - { - id: 'node', - name: 'Node.js', - description: "JavaScript runtime built on Chrome's V8 engine", - templates: [ - { - name: 'gill-node-express', - description: 'Node Express server with gill', - repository: 'gh:solana-foundation/templates/templates/template-node-express', - }, - { - name: 'gill-node-script', - description: 'Simple Node script with gill', - repository: 'gh:solana-foundation/templates/templates/template-node-script', - }, - ], - }, - { - id: 'react-vite', - name: 'React with Vite', - description: 'React with Vite and React Router', - templates: [ - { - name: 'gill-react-vite-tailwind', - description: 'React with Vite, Tailwind, gill (based on @solana/kit), Wallet UI', - repository: 'gh:solana-foundation/templates/templates/template-react-vite-tailwind', - }, - { - name: 'gill-react-vite-tailwind-basic', - description: 'React with Vite, Tailwind, basic Anchor example', - repository: 'gh:solana-foundation/templates/templates/template-react-vite-tailwind-basic', - }, - { - name: 'gill-react-vite-tailwind-counter', - description: 'React with Vite, Tailwind, Anchor Counter Example', - repository: 'gh:solana-foundation/templates/templates/template-react-vite-tailwind-counter', - }, - { - name: 'web3js-react-vite-tailwind', - description: 'React with Vite, Tailwind, @solana/web3.js, Wallet Adapter', - repository: 'gh:solana-foundation/templates/legacy/legacy-react-vite-tailwind', - }, - { - name: 'web3js-react-vite-tailwind-basic', - description: 'React with Vite, Tailwind, basic Anchor example, @solana/web3.js, Wallet Adapter', - repository: 'gh:solana-foundation/templates/legacy/legacy-react-vite-tailwind-basic', - }, - { - name: 'web3js-react-vite-tailwind-counter', - description: 'React with Vite, Tailwind, Anchor Counter Example, @solana/web3.js, Wallet Adapter', - repository: 'gh:solana-foundation/templates/legacy/legacy-react-vite-tailwind-counter', - }, - ], - }, - { - id: 'solana-mobile', - name: 'Solana Mobile', - description: 'Solana Mobile Templates based on Expo', - templates: [ - { - name: 'web3js-expo', - description: 'Expo React Native app with Mobile Wallet Adapter and @solana/web3.js', - repository: 'gh:solana-foundation/templates/mobile/web3js-expo', - }, - { - name: 'web3js-expo-paper', - description: 'Expo React Native Paper app with Mobile Wallet Adapter and @solana/web3.js', - repository: 'gh:solana-foundation/templates/mobile/web3js-expo-paper', - }, - ], - }, -] diff --git a/src/templates/templates.ts b/src/templates/templates.ts deleted file mode 100644 index d6a6ad92..00000000 --- a/src/templates/templates.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { log } from '@clack/prompts' -import { Framework, frameworks } from './frameworks' - -export const templates = getTemplatesForFrameworks(frameworks) - -export interface Template { - name: string - description: string - repository: string -} - -export function findTemplate(name: string): Template { - // A template name with a `/` is considered external - if (name.includes('/')) { - return { - name, - description: `${name} (external)`, - repository: name.includes(':') ? name : `gh:${name}`, - } - } - - const template: Template | undefined = templates.find((template) => template.name === name) - - if (!template) { - throw new Error(`Template ${name} not found`) - } - return template -} - -function getTemplatesForFrameworks(frameworks: Framework[] = []): Template[] { - return frameworks.reduce((acc, item) => { - return [...acc, ...getTemplatesForFramework(item)] - }, [] as Template[]) -} - -export function getTemplatesForFramework(framework: Framework): Template[] { - return framework.templates -} - -export function listTemplates() { - for (const template of templates) { - log.info(`${template.name}: \n\n\t${template.description}\n\t${template.repository}`) - } -} diff --git a/src/utils/create-app-task-clone-template.ts b/src/utils/create-app-task-clone-template.ts index 9387b1ce..de638559 100644 --- a/src/utils/create-app-task-clone-template.ts +++ b/src/utils/create-app-task-clone-template.ts @@ -14,20 +14,20 @@ export function createAppTaskCloneTemplate(args: GetArgsResult): Task { if (exists) { taskFail(`Target directory ${args.targetDirectory} already exists`) } - if (!args.template.repository) { - taskFail('No template repository specified') + if (!args.template.id) { + taskFail('No template id specified') } if (args.verbose) { - log.warn(`Cloning template ${args.template.repository} to ${args.targetDirectory}`) + log.warn(`Cloning template ${args.template.id} to ${args.targetDirectory}`) } try { - const { dir } = await downloadTemplate(args.template.repository, { + const { dir } = await downloadTemplate(args.template.id, { dir: args.targetDirectory, }) // make sure the dir is not empty const files = await readdir(dir) if (files.length === 0) { - taskFail(`The template directory is empty. Please check the repository: ${args.template.repository}`) + taskFail(`The template directory is empty. Please check the template id: ${args.template.id}`) return } diff --git a/src/utils/fetch-template-data.ts b/src/utils/fetch-template-data.ts new file mode 100644 index 00000000..7cc59667 --- /dev/null +++ b/src/utils/fetch-template-data.ts @@ -0,0 +1,23 @@ +import { MenuConfig, MenuItem, TemplateJsonTemplate } from '@beeman/repokit' +import { getTemplateGroupsFromUrl } from './get-template-groups-from-url' +import { getMenuItemsFromTemplateGroups } from './get-menu-items-from-template-groups' +import { getTemplatesFromItems } from './get-templates-from-items' + +export async function fetchTemplateData({ + config, + url, + verbose, +}: { + config: MenuConfig + url: string + verbose: boolean +}): Promise<{ + items: MenuItem[] + templates: TemplateJsonTemplate[] +}> { + const groups = await getTemplateGroupsFromUrl({ url, verbose }) + const items = getMenuItemsFromTemplateGroups({ config, groups, verbose }) + const templates = getTemplatesFromItems({ items, verbose }) + + return { items, templates } +} diff --git a/src/utils/find-template.ts b/src/utils/find-template.ts new file mode 100644 index 00000000..dda6ef5f --- /dev/null +++ b/src/utils/find-template.ts @@ -0,0 +1,36 @@ +import { TemplateJsonTemplate } from '@beeman/repokit' +import { log } from '@clack/prompts' + +import { Template } from './template' + +export function findTemplate({ + name, + templates, + verbose, +}: { + name: string + templates: TemplateJsonTemplate[] + verbose: boolean +}): Template { + // A template name with a `/` is considered external + if (name.includes('/')) { + if (verbose) { + log.warning(`Provided template contains a '/' so we treat it as an external template`) + } + return { + name, + description: `${name} (external)`, + id: name.includes(':') ? name : `gh:${name}`, + } + } + + const template: Template | undefined = templates.find((template) => template.name === name) + + if (!template) { + throw new Error(`Template ${name} not found`) + } + if (verbose) { + log.warning(`Found template ${name}: ${JSON.stringify(template, undefined, 2)}`) + } + return template +} diff --git a/src/utils/get-args-result.ts b/src/utils/get-args-result.ts index dbffdf21..42362cfb 100644 --- a/src/utils/get-args-result.ts +++ b/src/utils/get-args-result.ts @@ -1,6 +1,6 @@ -import { Template } from '../templates/templates' import { AppInfo } from './get-app-info' import { PackageManager } from './vendor/package-manager' +import { Template } from './template' export interface GetArgsResult { app: AppInfo diff --git a/src/utils/get-args.ts b/src/utils/get-args.ts index f5c17164..07cd5d84 100644 --- a/src/utils/get-args.ts +++ b/src/utils/get-args.ts @@ -1,13 +1,18 @@ import { intro, log, outro } from '@clack/prompts' import { program } from 'commander' import * as process from 'node:process' -import { findTemplate, listTemplates, Template } from '../templates/templates' +import { fetchTemplateData } from './fetch-template-data' import { AppInfo } from './get-app-info' import { GetArgsResult } from './get-args-result' import { getPrompts } from './get-prompts' import { listVersions } from './list-versions' import { runVersionCheck } from './run-version-check' import { PackageManager } from './vendor/package-manager' +import { getTemplatesUrl } from './get-templates-url' +import { findTemplate } from './find-template' +import { getMenuConfig } from './get-menu-config' +import { listTemplates } from './list-templates' +import { Template } from './template' export async function getArgs(argv: string[], app: AppInfo, pm: PackageManager = 'npm'): Promise { // Get the result from the command line @@ -27,6 +32,7 @@ export async function getArgs(argv: string[], app: AppInfo, pm: PackageManager = .option('--skip-init', help('Skip running the init script')) .option('--skip-install', help('Skip installing dependencies')) .option('--skip-version-check', help('Skip checking for CLI updates (not recommended)')) + .option('--templates-url', help('Url to templates.json'), getTemplatesUrl()) .option('-v, --verbose', help('Verbose output (default: false)')) .helpOption('-h, --help', help('Display help for command')) .addHelpText( @@ -47,12 +53,16 @@ Examples: const result = input.opts() const verbose = result.verbose ?? false + // Fetch the templates url, parse the template data and create menu items following our menu config + const { templates, items } = await fetchTemplateData({ config: getMenuConfig(), url: result.templatesUrl, verbose }) + if (result.listVersions) { listVersions() process.exit(0) } + if (result.listTemplates) { - listTemplates() + listTemplates({ templates }) outro( `\uD83D\uDCA1 To use a template, run "${app.name}${name ? ` ${name}` : ''} --template " or "--template /" `, ) @@ -86,7 +96,7 @@ Examples: let template: Template | undefined if (result.template) { - template = findTemplate(result.template) + template = findTemplate({ name: result.template, templates, verbose }) } // Take the result from the command line and use it to populate the options @@ -105,7 +115,7 @@ Examples: } // Get the prompts for any missing options - const prompts = await getPrompts({ options: options as GetArgsResult }) + const prompts = await getPrompts({ items, options: options as GetArgsResult }) // Populate the options with the prompts if (prompts.name) { diff --git a/src/utils/get-menu-config.ts b/src/utils/get-menu-config.ts new file mode 100644 index 00000000..ce236efb --- /dev/null +++ b/src/utils/get-menu-config.ts @@ -0,0 +1,42 @@ +import { MenuConfig } from '@beeman/repokit' + +// This configuration defines the menu options of the CLI and the groups and keywords they filter on. +export function getMenuConfig(): MenuConfig { + return [ + { + description: 'A React framework by Vercel', + groups: ['templates', 'legacy', 'gill', 'web3js'], + id: 'next', + keywords: ['nextjs'], + name: 'Next.js', + }, + { + description: "JavaScript runtime built on Chrome's V8 engine", + groups: ['templates', 'legacy', 'gill', 'web3js'], + id: 'node', + name: 'Node.js', + keywords: ['node'], + }, + { + description: 'React with Vite and React Router', + groups: ['templates', 'legacy', 'gill', 'web3js'], + id: 'react-vite', + keywords: ['react', 'vite'], + name: 'React with Vite', + }, + { + description: 'Solana Mobile Templates based on Expo', + groups: ['mobile'], + id: 'solana-mobile', + keywords: ['expo'], + name: 'Solana Mobile', + }, + { + description: 'Templates created by the community (unsupported)', + groups: ['community'], + id: 'community', + keywords: [], + name: 'Community', + }, + ] +} diff --git a/src/utils/get-menu-items-from-template-groups.ts b/src/utils/get-menu-items-from-template-groups.ts new file mode 100644 index 00000000..057c645a --- /dev/null +++ b/src/utils/get-menu-items-from-template-groups.ts @@ -0,0 +1,21 @@ +import { getMenuItems, MenuConfig, TemplateJsonGroup } from '@beeman/repokit' +import { log } from '@clack/prompts' +import pico from 'picocolors' + +export function getMenuItemsFromTemplateGroups({ + config, + groups, + verbose, +}: { + config: MenuConfig + groups: TemplateJsonGroup[] + verbose: boolean +}) { + const items = getMenuItems({ config, groups }) + if (verbose) { + log.warning( + `Got ${items.length} menu items:\n- ${items.map((item) => `${pico.cyan(item.name)} (${pico.gray(item.id)})`).join('\n- ')}`, + ) + } + return items +} diff --git a/src/utils/get-prompt-template.ts b/src/utils/get-prompt-template.ts index d578ce13..d9b20533 100644 --- a/src/utils/get-prompt-template.ts +++ b/src/utils/get-prompt-template.ts @@ -1,29 +1,34 @@ import { isCancel, log, select, SelectOptions } from '@clack/prompts' -import { Framework, frameworks } from '../templates/frameworks' -import { getTemplatesForFramework, Template } from '../templates/templates' import { GetArgsResult } from './get-args-result' +import { MenuItem } from '@beeman/repokit' +import { Template } from './template' -export function getPromptTemplate({ options }: { options: GetArgsResult }) { +export function getPromptTemplate({ items, options }: { items: MenuItem[]; options: GetArgsResult }) { return async () => { if (options.template) { log.success(`Template: ${options.template.description}`) return options.template } - const framework: Framework = await selectFramework(frameworks) - if (isCancel(framework)) { - throw 'No framework selected' + const group: MenuItem = await selectGroup(items) + if (isCancel(group)) { + throw 'No group selected' } - return selectTemplate(getTemplatesForFramework(framework)) + return selectTemplate(group.templates) } } -function getFrameworkSelectOptions( - values: Framework[], -): SelectOptions<{ value: Framework; label: string; hint?: string | undefined }[], Framework> { +function getGroupSelectOptions(values: MenuItem[]): SelectOptions< + { + value: MenuItem + label: string + hint?: string | undefined + }[], + MenuItem +> { return { - message: 'Select a framework', + message: 'Select a group', options: values.map((value) => ({ label: value.name, value, @@ -32,8 +37,8 @@ function getFrameworkSelectOptions( } } -function selectFramework(values: Framework[]): Promise { - return select(getFrameworkSelectOptions(values)) as Promise +function selectGroup(values: MenuItem[]): Promise { + return select(getGroupSelectOptions(values)) as Promise } function getTemplateSelectOptions( diff --git a/src/utils/get-prompts.ts b/src/utils/get-prompts.ts index 6d915dbd..930d24a9 100644 --- a/src/utils/get-prompts.ts +++ b/src/utils/get-prompts.ts @@ -3,12 +3,13 @@ import * as process from 'node:process' import { GetArgsResult } from './get-args-result' import { getPromptName } from './get-prompt-name' import { getPromptTemplate } from './get-prompt-template' +import { MenuItem } from '@beeman/repokit' -export function getPrompts({ options }: { options: GetArgsResult }) { +export function getPrompts({ items, options }: { items: MenuItem[]; options: GetArgsResult }) { return group( { name: getPromptName({ options }), - template: getPromptTemplate({ options }), + template: getPromptTemplate({ items, options }), }, { onCancel: () => { diff --git a/src/utils/get-template-groups-from-url.ts b/src/utils/get-template-groups-from-url.ts new file mode 100644 index 00000000..73ec124f --- /dev/null +++ b/src/utils/get-template-groups-from-url.ts @@ -0,0 +1,16 @@ +import { log } from '@clack/prompts' +import pico from 'picocolors' +import { fetchTemplatesJsonUrl } from '@beeman/repokit' + +export async function getTemplateGroupsFromUrl({ url, verbose }: { url: string; verbose: boolean }) { + if (verbose) { + log.warning(`Fetching templates url: ${pico.cyan(url)}`) + } + const groups = await fetchTemplatesJsonUrl(url) + if (verbose) { + log.warning( + `Fetched ${groups.length} groups from remote url:\n- ${groups.map((group) => `${pico.cyan(group.name)} (${pico.gray(group.path)})`).join('\n- ')}`, + ) + } + return groups +} diff --git a/src/utils/get-templates-from-items.ts b/src/utils/get-templates-from-items.ts new file mode 100644 index 00000000..c8d75043 --- /dev/null +++ b/src/utils/get-templates-from-items.ts @@ -0,0 +1,19 @@ +import { MenuItem, TemplateJsonTemplate } from '@beeman/repokit' +import { log } from '@clack/prompts' +import pico from 'picocolors' + +export function getTemplatesFromItems({ + items, + verbose, +}: { + items: MenuItem[] + verbose: boolean +}): TemplateJsonTemplate[] { + const templates = items?.flatMap((i) => i.templates) + if (verbose) { + log.warning( + `Got ${templates.length} templates:\n- ${templates.map((t) => `${pico.cyan(t.name)} (${pico.gray(t.id)})`).join('\n- ')}`, + ) + } + return templates +} diff --git a/src/utils/get-templates-url.ts b/src/utils/get-templates-url.ts new file mode 100644 index 00000000..cacd20a1 --- /dev/null +++ b/src/utils/get-templates-url.ts @@ -0,0 +1,6 @@ +export function getTemplatesUrl() { + return ( + process.env.TEMPLATES_URL ?? + `https://raw.githubusercontent.com/solana-foundation/templates/refs/heads/main/templates.json` + ) +} diff --git a/src/utils/list-templates.ts b/src/utils/list-templates.ts new file mode 100644 index 00000000..842b5ea3 --- /dev/null +++ b/src/utils/list-templates.ts @@ -0,0 +1,8 @@ +import { TemplateJsonTemplate } from '@beeman/repokit' +import { log } from '@clack/prompts' + +export function listTemplates({ templates }: { templates: TemplateJsonTemplate[] }) { + for (const template of templates) { + log.info(`${template.name}: \n\n\t${template.description}\n\t${template.id}`) + } +} diff --git a/src/utils/template.ts b/src/utils/template.ts new file mode 100644 index 00000000..6fb4009b --- /dev/null +++ b/src/utils/template.ts @@ -0,0 +1,6 @@ +import { TemplateJsonTemplate } from '@beeman/repokit' + +export interface Template extends Omit { + keywords?: string[] + path?: string +}