From d71359f8a2b62e3e69f9aaa4c1d273821b592317 Mon Sep 17 00:00:00 2001 From: Alexey Kasperovich Date: Wed, 11 Mar 2026 16:04:02 +0300 Subject: [PATCH 1/7] fix: missing dependency --- template/apps/web/package.json | 1 + template/pnpm-lock.yaml | 14 ++++++++++++++ 2 files changed, 15 insertions(+) diff --git a/template/apps/web/package.json b/template/apps/web/package.json index 247ca0ea4..6adf4dfcf 100644 --- a/template/apps/web/package.json +++ b/template/apps/web/package.json @@ -52,6 +52,7 @@ "react-dom": "catalog:", "react-dropzone": "15.0.0", "react-hook-form": "7.57.0", + "react-icons": "5.6.0", "shared": "workspace:*", "socket.io-client": "4.7.5", "sonner": "^1.7.4", diff --git a/template/pnpm-lock.yaml b/template/pnpm-lock.yaml index 909f897c1..04cae9c3e 100644 --- a/template/pnpm-lock.yaml +++ b/template/pnpm-lock.yaml @@ -340,6 +340,9 @@ importers: react-hook-form: specifier: 7.57.0 version: 7.57.0(react@19.0.3) + react-icons: + specifier: 5.6.0 + version: 5.6.0(react@19.0.3) shared: specifier: workspace:* version: link:../../packages/shared @@ -2168,6 +2171,7 @@ packages: '@koa/router@14.0.0': resolution: {integrity: sha512-LBSu5K0qAaaQcXX/0WIB9PGDevyCxxpnc1uq13vV/CgObaVxuis5hKl3Eboq/8gcb6ebnkAStW9NB/Em2eYyFA==} engines: {node: '>= 20'} + deprecated: Please upgrade to v15 or higher. All reported bugs in this version are fixed in newer releases, dependencies have been updated, and security has been improved. '@mdx-js/esbuild@3.1.1': resolution: {integrity: sha512-NS35VhTdvKNj5/B1JSD5W3kN1R0WDHgk+zCWq+tSChQw5L2Bgeiz7yyZPFrc5LWuPVOxE1xMbJr82bO9VVzmfQ==} @@ -5326,6 +5330,7 @@ packages: glob@11.0.3: resolution: {integrity: sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==} engines: {node: 20 || >=22} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me hasBin: true globals@14.0.0: @@ -6649,6 +6654,11 @@ packages: peerDependencies: react: ^16.8.0 || ^17 || ^18 || ^19 + react-icons@5.6.0: + resolution: {integrity: sha512-RH93p5ki6LfOiIt0UtDyNg/cee+HLVR6cHHtW3wALfo+eOHTp8RnU2kRkI6E+H19zMIs03DyxUG/GfZMOGvmiA==} + peerDependencies: + react: '*' + react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} @@ -15192,6 +15202,10 @@ snapshots: dependencies: react: 19.0.3 + react-icons@5.6.0(react@19.0.3): + dependencies: + react: 19.0.3 + react-is@16.13.1: {} react-promise-suspense@0.3.4: From 0f46588fa4af30edeb9f0c4dcaa4630d6c4d09f0 Mon Sep 17 00:00:00 2001 From: Alexey Kasperovich Date: Wed, 11 Mar 2026 16:28:52 +0300 Subject: [PATCH 2/7] feat: plugin docs --- docs/docs.json | 8 ++ docs/packages/create-ship-app.mdx | 18 ++++- docs/plugins/ai-chat.mdx | 88 ++++++++++++++++++++++ docs/plugins/overview.mdx | 83 +++++++++++++++++++++ docs/plugins/stripe-subscriptions.mdx | 101 ++++++++++++++++++++++++++ 5 files changed, 297 insertions(+), 1 deletion(-) create mode 100644 docs/plugins/ai-chat.mdx create mode 100644 docs/plugins/overview.mdx create mode 100644 docs/plugins/stripe-subscriptions.mdx diff --git a/docs/docs.json b/docs/docs.json index 51f969d4e..b2f9e1281 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -106,6 +106,14 @@ }, "packages/create-ship-app" ] + }, + { + "group": "Plugins", + "pages": [ + "plugins/overview", + "plugins/stripe-subscriptions", + "plugins/ai-chat" + ] } ] } diff --git a/docs/packages/create-ship-app.mdx b/docs/packages/create-ship-app.mdx index b254fd7a8..ef78e9382 100644 --- a/docs/packages/create-ship-app.mdx +++ b/docs/packages/create-ship-app.mdx @@ -28,4 +28,20 @@ or ```shell npx create-ship-app@latest my-project -``` \ No newline at end of file +``` + +## Plugins + +The CLI also supports installing pre-built features via the plugin system: + +```shell +npx create-ship-app@latest install +``` + +For example: + +```shell +npx create-ship-app@latest install stripe-subscriptions +``` + +See the [Plugins documentation](/plugins/overview) for the full list of available plugins and how they work. \ No newline at end of file diff --git a/docs/plugins/ai-chat.mdx b/docs/plugins/ai-chat.mdx new file mode 100644 index 000000000..5af573942 --- /dev/null +++ b/docs/plugins/ai-chat.mdx @@ -0,0 +1,88 @@ +--- +title: "AI Chat" +--- + +The `ai-chat` plugin adds an AI-powered chat interface to your Ship project. It features real-time streaming responses using Google Gemini, persistent chat history stored in MongoDB, and a polished chat UI with multiple conversation support. + +## Installation + +```shell +npx create-ship-app@latest install ai-chat +``` + +## What gets installed + +### Dependencies + +| Workspace | Package | Version | +|---|---|---| +| `apps/api` | `@ai-sdk/google` | `3.0.29` | +| `apps/api` | `ai` | `6.0.86` | +| `apps/web` | `@ai-sdk/react` | `3.0.88` | + +### Files + +**API (`apps/api`):** + +| File | Description | +|---|---| +| `src/config/ai.config.ts` | Environment config for `GOOGLE_GENERATIVE_AI_API_KEY` | +| `src/services/ai/ai.service.ts` | AI service using Google Gemini (`gemini-2.5-flash`) with streaming support | +| `src/resources/ai-chat/ai-chat.schema.ts` | Zod schema for chat conversations | +| `src/resources/ai-chat/ai-chat.service.ts` | Database service for chat CRUD operations | +| `src/resources/ai-chat/ai-message.schema.ts` | Zod schema for individual messages (role, content) | +| `src/resources/ai-chat/ai-message.service.ts` | Database service for message CRUD operations | +| `src/resources/ai-chat/endpoints/create.ts` | Create a new chat conversation | +| `src/resources/ai-chat/endpoints/list.ts` | List all chats for the current user | +| `src/resources/ai-chat/endpoints/delete.ts` | Delete a chat and its messages | +| `src/resources/ai-chat/endpoints/get-messages.ts` | Get all messages in a chat | +| `src/resources/ai-chat/endpoints/send-message.ts` | Send a message and stream the AI response via SSE | +| `src/resources/ai-chat/index.ts` | Barrel export for services | + +**Web (`apps/web`):** + +| File | Description | +|---|---| +| `src/components/ui/prompt-input.tsx` | Reusable prompt input component with auto-resize and keyboard shortcuts | +| `src/pages/ai-chat/index.page.tsx` | Main AI chat page with conversation sidebar | +| `src/pages/ai-chat/[chatId].page.tsx` | Individual chat page (route handler) | +| `src/pages/ai-chat/components/AiChatBox.tsx` | Chat container with message list and input | +| `src/pages/ai-chat/components/AiChatInput.tsx` | Chat input with send button | +| `src/pages/ai-chat/components/AiChatMessage.tsx` | Individual message bubble (user/assistant) | +| `src/pages/ai-chat/components/AiMessageSkeleton.tsx` | Loading skeleton for messages | +| `src/pages/ai-chat/hooks/useAiChatManager.ts` | Hook managing chat state, SSE streaming, and message history | + +## Environment variables + +### API (`apps/api`) + +| Variable | Description | +|---|---| +| `GOOGLE_GENERATIVE_AI_API_KEY` | Google AI API key from [Google AI Studio](https://aistudio.google.com/apikey) | + +## How it works + +### Streaming responses + +The plugin uses **Server-Sent Events (SSE)** for real-time AI response streaming: + +1. The user sends a message via `POST /:chatId/messages`. +2. The API saves the user message, loads the full conversation history, and calls Google Gemini. +3. The response is streamed back chunk by chunk as SSE events. +4. Once complete, the assistant message is saved to the database. + +The frontend `useAiChatManager` hook handles parsing the SSE stream and updating the UI in real time. + +### Chat management + +Each user can have multiple chat conversations. Chats are automatically titled based on the first message. All data is stored in MongoDB via Ship's data service layer. + +## API endpoints + +| Method | Path | Description | +|---|---|---| +| `GET` | `/ai-chat/` | List all chats for the current user | +| `POST` | `/ai-chat/` | Create a new chat | +| `DELETE` | `/ai-chat/:chatId` | Delete a chat and its messages | +| `GET` | `/ai-chat/:chatId/messages` | Get messages for a chat | +| `POST` | `/ai-chat/:chatId/messages` | Send a message and stream AI response | diff --git a/docs/plugins/overview.mdx b/docs/plugins/overview.mdx new file mode 100644 index 000000000..e717e6d8c --- /dev/null +++ b/docs/plugins/overview.mdx @@ -0,0 +1,83 @@ +--- +title: "Overview" +--- + +Plugins are pre-built features that can be installed into your Ship project with a single command. They add new functionality by scaffolding files across your monorepo (`apps/api`, `apps/web`, `packages/shared`) and merging the required dependencies into the respective `package.json` files. + +Plugins are hosted in the public [ship-plugins](https://github.com/paralect/ship-plugins) repository and installed via the `create-ship-app` CLI. + +## Installing a plugin + +Run the following command from your project root: + +```shell +npx create-ship-app@latest install +``` + +For example: + +```shell +npx create-ship-app@latest install stripe-subscriptions +``` + +The install process will: + +1. **Download** the plugin files from the [ship-plugins](https://github.com/paralect/ship-plugins) repository. +2. **Copy files** into your project, preserving the directory structure. Existing files are never overwritten. +3. **Merge dependencies** declared in the plugin manifest into the appropriate workspace `package.json` files. +4. **Run post-install** — executes `pnpm install` and `pnpm run --filter shared generate` to install new dependencies and regenerate shared schemas. + +## Available plugins + +| Plugin | Description | +|---|---| +| [stripe-subscriptions](/plugins/stripe-subscriptions) | Subscription billing with Stripe Checkout, webhooks, and a pricing page | +| [ai-chat](/plugins/ai-chat) | AI chat with streaming responses powered by Google Gemini | + +## Plugin structure + +Each plugin in the [ship-plugins](https://github.com/paralect/ship-plugins) repository follows the same convention: + +``` +/ +├── package.json # Plugin manifest (name, version, dependencies) +└── apps/ + ├── api/ # API-side files (configs, resources, services) + └── web/ # Web-side files (pages, components, configs) +``` + +The `package.json` manifest declares which npm packages should be added to which workspace. For example: + +```json +{ + "name": "ship-plugin-stripe-subscriptions", + "version": "1.0.0", + "private": true, + "dependencies": { + "apps/api": { + "stripe": "20.3.1" + } + } +} +``` + +This tells the installer to add `stripe@20.3.1` to `apps/api/package.json`. + +## Creating a plugin + +Ship also provides a command to package your own feature as a plugin from the last git commit: + +```shell +npx create-ship-app@latest create-plugin +``` + +This will: + +1. Extract new files added in the last commit. +2. Detect dependency changes from modified workspace `package.json` files. +3. Package everything into the plugin format. +4. Push to a new branch on the [ship-plugins](https://github.com/paralect/ship-plugins) repository and provide a PR link. + + + You need push access to the [ship-plugins](https://github.com/paralect/ship-plugins) repository to use `create-plugin`. + diff --git a/docs/plugins/stripe-subscriptions.mdx b/docs/plugins/stripe-subscriptions.mdx new file mode 100644 index 000000000..1f6ea584e --- /dev/null +++ b/docs/plugins/stripe-subscriptions.mdx @@ -0,0 +1,101 @@ +--- +title: "Stripe Subscriptions" +--- + +The `stripe-subscriptions` plugin adds subscription-based billing to your Ship project using [Stripe](https://stripe.com). It includes a complete pricing page, Stripe Checkout integration, webhook handling, and subscription management APIs. + +## Installation + +```shell +npx create-ship-app@latest install stripe-subscriptions +``` + +## What gets installed + +### Dependencies + +| Workspace | Package | Version | +|---|---|---| +| `apps/api` | `stripe` | `20.3.1` | + +### Files + +**API (`apps/api`):** + +| File | Description | +|---|---| +| `src/config/stripe.config.ts` | Environment config for `STRIPE_SECRET_KEY` and `STRIPE_WEBHOOK_SECRET` | +| `src/services/stripe/stripe.service.ts` | Stripe SDK wrapper — customer creation, subscription helpers, billing portal access | +| `src/resources/subscriptions/subscription.schema.ts` | Zod schema for subscription data | +| `src/resources/subscriptions/endpoints/subscribe.ts` | Creates a Stripe Checkout session for new subscriptions | +| `src/resources/subscriptions/endpoints/getCurrent.ts` | Returns the current user's active subscription with live Stripe sync | +| `src/resources/subscriptions/endpoints/upgrade.ts` | Upgrades/downgrades/cancels the current subscription | +| `src/resources/subscriptions/endpoints/previewUpgrade.ts` | Previews proration costs before upgrading | +| `src/resources/subscriptions/endpoints/createPortalSession.ts` | Creates a Stripe Billing Portal session | +| `src/resources/webhook/endpoints/stripe.ts` | Handles Stripe webhook events (subscription created/updated/deleted) | +| `src/resources/users/user.stripe.extend.ts` | Extends the user schema with `stripeId` and `subscription` fields | + +**Web (`apps/web`):** + +| File | Description | +|---|---| +| `src/config/stripe.config.ts` | Client-side config for Stripe public key and price IDs | +| `src/pages/pricing/constants.ts` | Plan definitions (Free, Creator, Pro) with price IDs | +| `src/pages/pricing/index.page.tsx` | Full pricing page with plan selection, subscribe, upgrade, and billing portal | + +## Environment variables + +After installing, add the following environment variables: + +### API (`apps/api`) + +| Variable | Description | +|---|---| +| `STRIPE_SECRET_KEY` | Your Stripe secret key from the [Developers dashboard](https://dashboard.stripe.com/test/developers) | +| `STRIPE_WEBHOOK_SECRET` | Webhook signing secret from your Stripe webhook endpoint | + +### Web (`apps/web`) + +| Variable | Description | +|---|---| +| `NEXT_PUBLIC_STRIPE_PUBLIC_KEY` | Your Stripe publishable key | +| `NEXT_PUBLIC_PRICE_CREATOR` | Stripe Price ID for the Creator plan | +| `NEXT_PUBLIC_PRICE_PRO` | Stripe Price ID for the Pro plan | + +## Stripe setup + +### 1. Get API keys + +Navigate to the [Developers](https://dashboard.stripe.com/test/developers) tab and select **API keys**. + +- **Publishable key** → `NEXT_PUBLIC_STRIPE_PUBLIC_KEY` (web) +- **Secret key** → `STRIPE_SECRET_KEY` (api) + +### 2. Create subscription products + +Go to the [Products](https://dashboard.stripe.com/test/products) tab and create your subscription products. Each product needs a recurring price. Copy the Price IDs into your web environment variables. + +### 3. Set up webhooks + +Create a webhook endpoint pointing to `https:///webhook/stripe` with the following events: + +- `customer.subscription.created` +- `customer.subscription.updated` +- `customer.subscription.deleted` + +Copy the webhook signing secret into `STRIPE_WEBHOOK_SECRET`. + + + For local development, use the [Stripe CLI](https://stripe.com/docs/stripe-cli) to forward webhook events to your localhost. + + +## API endpoints + +| Method | Path | Description | +|---|---|---| +| `POST` | `/subscriptions/subscribe` | Create a Checkout session for a new subscription | +| `GET` | `/subscriptions/current` | Get the current user's subscription (syncs with Stripe) | +| `POST` | `/subscriptions/upgrade` | Change plan or cancel subscription | +| `GET` | `/subscriptions/preview-upgrade` | Preview proration costs | +| `POST` | `/subscriptions/create-portal-session` | Open the Stripe Billing Portal | +| `POST` | `/webhook/stripe` | Stripe webhook handler (public endpoint) | From f485ac5faef16590d71e3ea0dbd494ba284b52f9 Mon Sep 17 00:00:00 2001 From: Alexey Kasperovich Date: Wed, 11 Mar 2026 16:40:34 +0300 Subject: [PATCH 3/7] chore: remove old cli commands --- .../src/helpers/create-resource.ts | 152 ------------ packages/create-ship-app/src/helpers/index.ts | 1 - .../create-ship-app/src/helpers/lintDir.ts | 22 -- .../src/helpers/resources-templates.ts | 222 ------------------ packages/create-ship-app/src/index.ts | 12 - 5 files changed, 409 deletions(-) delete mode 100644 packages/create-ship-app/src/helpers/create-resource.ts delete mode 100644 packages/create-ship-app/src/helpers/lintDir.ts delete mode 100644 packages/create-ship-app/src/helpers/resources-templates.ts diff --git a/packages/create-ship-app/src/helpers/create-resource.ts b/packages/create-ship-app/src/helpers/create-resource.ts deleted file mode 100644 index ebfd8c3dc..000000000 --- a/packages/create-ship-app/src/helpers/create-resource.ts +++ /dev/null @@ -1,152 +0,0 @@ -import fs from 'fs'; -import path from 'path'; -import pluralize from 'pluralize'; - -import config from 'config'; - -import { lintDir } from './lintDir'; -import { - actionCreateContent, - actionGetContent, - actionRemoveContent, - actionUpdateContent, - indexContent, - routesContent, - schemaContent, - serviceContent, - typeContent, - webApiIndexContent, - webResourceContent, -} from './resources-templates'; - -const createDirectory = (dirPath: string) => { - if (!fs.existsSync(dirPath)) { - fs.mkdirSync(dirPath, { recursive: true }); - console.log(`Directory created: ${dirPath}`); - } else { - console.error(`Directory already exists: ${dirPath}`); - } -}; - -const createFile = (filePath: string, content = '') => { - if (!fs.existsSync(filePath)) { - fs.writeFileSync(filePath, content); - console.log(`File created: ${filePath}`); - } else { - console.error(`File already exists: ${filePath}`); - } -}; - -const modifyPrivateRoutes = (name: string, cwd: string) => { - const filePath = path.join(cwd, 'apps', 'api', 'src', 'routes', 'private.routes.ts'); - const importStatement = `import { ${name}Routes } from 'resources/${name}';\n`; - const routeUseStatement = ` app.use(mount('/${pluralize.plural( - name, - )}', compose([auth, ${name}Routes.privateRoutes])));`; - - try { - let data = fs.readFileSync(filePath, 'utf8'); - - const lines = data.split('\n'); - - if (!data.includes(importStatement)) { - lines.splice(0, 0, importStatement); - } - - const exportDefaultIndex = lines.findIndex((line) => line.trim().startsWith('export default')); - - if (exportDefaultIndex > -1) { - let insertIndex = exportDefaultIndex; - - while (insertIndex < lines.length && !lines[insertIndex].trim().startsWith('}')) { - insertIndex++; - } - - lines.splice(insertIndex - 1, 0, routeUseStatement); - } - - data = lines.join('\n'); - - fs.writeFileSync(filePath, data, 'utf8'); - console.log('File private.routes.ts has been updated successfully.'); - } catch (err) { - console.error('Failed to update the private.routes.ts file:', err); - } -}; - -const addToPackageIndex = (directory: string, name: string, type: string) => { - const indexPath = path.join(directory, 'index.ts'); - const exportLine = `export * from './${name}.${type}';\n`; - - if (fs.existsSync(indexPath)) { - const data = fs.readFileSync(indexPath, 'utf8'); - const newData = exportLine + data; - - fs.writeFileSync(indexPath, newData); - } else { - fs.writeFileSync(indexPath, exportLine); - } -}; - -const updateApiConstants = (name: string, cwd: string) => { - const filePath = path.join(cwd, 'packages', 'app-constants', 'src', 'api.constants.ts'); - const content = fs.readFileSync(filePath, 'utf8').split('\n'); - - const dbIndex = content.findIndex((line) => line.includes('DATABASE_DOCUMENTS')); - - if (dbIndex !== -1) { - const entryLine = `${' '.repeat(2)}${pluralize.plural(name.toUpperCase())}: '${pluralize.plural(name)}',`; - - content.splice(dbIndex + 1, 0, entryLine); - - fs.writeFileSync(filePath, content.join('\n'), 'utf8'); - - console.log('Document added to DATABASE_DOCUMENTS successfully.'); - } else { - console.error('DATABASE_DOCUMENTS not found in api.constants.ts'); - } -}; - -export const createResource = async (name: string) => { - let cwd = process.cwd(); - - if (config.USE_LOCAL_REPO) { - cwd = path.join(cwd, '..', '..', 'template'); - } - - const resourcesDir = (resource: string) => path.join(cwd, 'apps', resource, 'src', 'resources', name); - const schemasDir = path.join(cwd, 'packages', 'schemas', 'src'); - const appTypesDir = path.join(cwd, 'packages', 'app-types', 'src'); - - updateApiConstants(name, cwd); - - createDirectory(resourcesDir('api')); - createDirectory(resourcesDir('web')); - createDirectory(path.join(resourcesDir('api'), 'actions')); - - const files = [ - { path: 'index.ts', content: indexContent(name), resource: 'api' }, - { path: `${name}.service.ts`, content: serviceContent(name), resource: 'api' }, - { path: `${name}.routes.ts`, content: routesContent(), resource: 'api' }, - { path: 'actions/create.ts', content: actionCreateContent(name), resource: 'api' }, - { path: 'actions/get.ts', content: actionGetContent(name), resource: 'api' }, - { path: 'actions/update.ts', content: actionUpdateContent(name), resource: 'api' }, - { path: 'actions/remove.ts', content: actionRemoveContent(name), resource: 'api' }, - { path: 'index.ts', content: webApiIndexContent(name), resource: 'web' }, - { path: `${name}.api.ts`, content: webResourceContent(name), resource: 'web' }, - ]; - - files.forEach(({ path: filePath, content, resource }) => { - createFile(path.join(resourcesDir(resource), filePath), content); - }); - - createFile(path.join(schemasDir, `${name}.schema.ts`), schemaContent(name)); - createFile(path.join(appTypesDir, `${name}.types.ts`), typeContent(name)); - - addToPackageIndex(schemasDir, name, 'schema'); - addToPackageIndex(appTypesDir, name, 'types'); - - modifyPrivateRoutes(name, cwd); - - await lintDir(cwd); -}; diff --git a/packages/create-ship-app/src/helpers/index.ts b/packages/create-ship-app/src/helpers/index.ts index e3a0e9b8d..62fca7853 100644 --- a/packages/create-ship-app/src/helpers/index.ts +++ b/packages/create-ship-app/src/helpers/index.ts @@ -1,5 +1,4 @@ export * from './common'; -export * from './create-resource'; export * from './git'; export * from './install'; export * from './is-folder-empty'; diff --git a/packages/create-ship-app/src/helpers/lintDir.ts b/packages/create-ship-app/src/helpers/lintDir.ts deleted file mode 100644 index 84fb8a0f8..000000000 --- a/packages/create-ship-app/src/helpers/lintDir.ts +++ /dev/null @@ -1,22 +0,0 @@ -import spawn from 'cross-spawn'; - -export const lintDir = async (cwd: string): Promise => - new Promise((resolve, reject) => { - const args: string[] = ['turbo', 'run', 'test:eslint', '--force']; - - const child = spawn('npx', args, { - stdio: 'inherit', - cwd, - }); - - child.on('close', (code) => { - if (code !== 0) { - const error = { command: `npx ${args.join(' ')}` }; - - reject(new Error(JSON.stringify(error))); - - return; - } - resolve(); - }); - }); diff --git a/packages/create-ship-app/src/helpers/resources-templates.ts b/packages/create-ship-app/src/helpers/resources-templates.ts deleted file mode 100644 index 8ca6e5cab..000000000 --- a/packages/create-ship-app/src/helpers/resources-templates.ts +++ /dev/null @@ -1,222 +0,0 @@ -import pluralize from 'pluralize'; - -const capitalizeFirstLetter = (string: string) => string.charAt(0).toUpperCase() + string.slice(1); - -export const serviceContent = (name: string) => `import db from 'db'; - -import { DATABASE_DOCUMENTS } from 'app-constants'; -import { ${name}Schema } from 'schemas'; -import { ${capitalizeFirstLetter(name)} } from 'types'; - -const service = db.createService<${capitalizeFirstLetter(name)}>(DATABASE_DOCUMENTS.${pluralize.plural( - name.toUpperCase(), -)}, { - schemaValidator: (obj) => ${name}Schema.parseAsync(obj), -}); - -export default service; -`; - -export const routesContent = () => `import { routeUtil } from 'utils'; - -import create from './actions/create'; -import get from './actions/get'; -import update from './actions/update'; -import remove from './actions/remove'; - -const publicRoutes = routeUtil.getRoutes([]); - -const privateRoutes = routeUtil.getRoutes([create, get, update, remove]); - -const adminRoutes = routeUtil.getRoutes([]); - -export default { - publicRoutes, - privateRoutes, - adminRoutes, -}; -`; - -export const indexContent = (name: string) => `import ${name}Routes from './${name}.routes'; -import ${name}Service from './${name}.service'; - -export { ${name}Routes, ${name}Service }; -`; - -export const actionCreateContent = (name: string) => `import { z } from 'zod'; - -import { ${name}Service } from 'resources/${name}'; - -import { AppKoaContext, AppRouter } from 'types'; - -import { validateMiddleware } from 'middlewares'; - -const schema = z.object({ - name: z.string().min(1, 'Please enter name').max(40), -}).strict(); - -type ValidatedData = z.infer; - -async function handler(ctx: AppKoaContext) { - const { name } = ctx.validatedData; - - const created${capitalizeFirstLetter(name)} = await ${name}Service.insertOne({ name }); - - ctx.body = created${capitalizeFirstLetter(name)}; -} - -export default (router: AppRouter) => { - router.post('/', validateMiddleware(schema), handler); -}; -`; - -export const actionGetContent = (name: string) => `import { AppKoaContext, AppRouter } from 'types'; - -import { ${name}Service } from 'resources/${name}'; - -type ValidatedData = never; -type Request = { - params: { - id: string; - }; -}; - -async function handler(ctx: AppKoaContext) { - const ${name} = await ${name}Service.findOne({ _id: ctx.request.params.id }); - - ctx.body = ${name}; -} - -export default (router: AppRouter) => { - router.get('/:id', handler); -}; -`; - -export const actionUpdateContent = (name: string) => `import { z } from 'zod'; - -import { ${name}Service } from 'resources/${name}'; - -import { AppKoaContext, AppRouter, Next } from 'types'; - -import { validateMiddleware } from 'middlewares'; - -const schema = z.object({ - name: z.string().min(1, 'Please enter name').max(40), -}); - -type ValidatedData = z.infer; -type Request = { - params: { - id: string; - }; -}; - -async function validator(ctx: AppKoaContext, next: Next) { - const { id } = ctx.request.params; - - const is${capitalizeFirstLetter(name)}Exists = await ${name}Service.exists({_id: id, deletedOn: { $exists: false } }); - - ctx.assertError(is${capitalizeFirstLetter(name)}Exists, '${capitalizeFirstLetter(name)} not found'); - - await next(); -} - -async function handler(ctx: AppKoaContext) { - const { id } = ctx.request.params; - const { name } = ctx.validatedData; - - const updated${capitalizeFirstLetter(name)} = await ${name}Service.updateOne({_id: id }, (prev) => ({...prev, name})); - - ctx.body = updated${capitalizeFirstLetter(name)}; -} - -export default (router: AppRouter) => { - router.put('/:id', validateMiddleware(schema), validator, handler); -}; -`; - -export const actionRemoveContent = (name: string) => `import { AppKoaContext, AppRouter, Next } from 'types'; - -import { ${name}Service } from 'resources/${name}'; - -type ValidatedData = never; -type Request = { - params: { - id: string; - }; -}; - -async function validator(ctx: AppKoaContext, next: Next) { - const { id } = ctx.request.params; - - const is${capitalizeFirstLetter(name)}Exists = await ${name}Service.exists({_id: id, deletedOn: { $exists: false } }); - - ctx.assertError(is${capitalizeFirstLetter(name)}Exists, '${capitalizeFirstLetter(name)} not found'); - - await next(); -} - -async function handler(ctx: AppKoaContext) { - const { id } = ctx.request.params; - - await ${name}Service.deleteSoft({ _id: id }); - - ctx.status = 204; -} - -export default (router: AppRouter) => { - router.delete('/:id', validator, handler); -}; -`; - -export const schemaContent = (name: string) => `import { z } from 'zod'; - -import dbSchema from './db.schema'; - -export const ${name}Schema = dbSchema - .extend({ - name: z.string(), - }) - .strict(); -`; - -export const typeContent = (name: string) => `import { z } from 'zod'; - -import { ${name}Schema } from 'schemas'; - -export type ${capitalizeFirstLetter(name)} = z.infer; -`; - -export const webApiIndexContent = (name: string) => `import * as ${name}Api from './${name}.api'; - -export { ${name}Api }; -`; - -export const webResourceContent = (name: string) => `import { useMutation, useQuery } from '@tanstack/react-query'; - -import { apiService } from 'services'; - -import { ${capitalizeFirstLetter(name)} } from 'types'; - -export const useCreate = () => - useMutation<${capitalizeFirstLetter(name)}, unknown, T>({ - mutationFn: (data: T) => apiService.post('/${pluralize(name)}', data), - }); - -export const useGet = (${name}Id: string, options = {}) => - useQuery<${capitalizeFirstLetter(name)}>({ - queryKey: ['${name}'], - queryFn: () => apiService.get(\`/${pluralize(name)}/\${${name}Id}\`), - ...options, - }); - -export const useUpdate = (${name}Id: string) => - useMutation<${capitalizeFirstLetter(name)}, unknown, T>({ - mutationFn: (data: T) => apiService.put(\`/${pluralize(name)}/\${${name}Id}\`, data), - }); - -export const useDelete = () => - useMutation({ - mutationFn: (${name}Id: string) => apiService.delete(\`/${pluralize(name)}/\${${name}Id}\`), - }); - `; diff --git a/packages/create-ship-app/src/index.ts b/packages/create-ship-app/src/index.ts index 8ea4670e3..d9797e022 100644 --- a/packages/create-ship-app/src/index.ts +++ b/packages/create-ship-app/src/index.ts @@ -10,7 +10,6 @@ import prompts from 'prompts'; import checkForUpdate from 'update-check'; import { - createResource, getDefaultProjectName, handleSigTerm, isFolderEmpty, @@ -64,17 +63,6 @@ Available deployment options: program.parse(process.argv); const run = async (): Promise => { - // rawArgs = ['create', 'resource', ]; - const isCommandCreateResource = rawArgs.length === 3 && rawArgs[0] === 'create' && rawArgs[1] === 'resource'; - - if (isCommandCreateResource) { - await createResource(rawArgs[2].toLowerCase()); - - console.log(`Resource ${rawArgs[2]} created successfully.`); - - return; - } - // rawArgs = ['install', ]; const isCommandInstall = rawArgs.length === 2 && rawArgs[0] === 'install'; From 98bec590973449d952c2a7c1eedd99634e002c34 Mon Sep 17 00:00:00 2001 From: Alexey Kasperovich Date: Wed, 11 Mar 2026 16:59:15 +0300 Subject: [PATCH 4/7] fix: readme npm, readme template --- packages/create-ship-app/README.md | 40 ++++++++++++++++++++++-------- template/README.md | 27 +++++++++++++++++++- 2 files changed, 55 insertions(+), 12 deletions(-) diff --git a/packages/create-ship-app/README.md b/packages/create-ship-app/README.md index 2be221bdf..83b6f9bd6 100644 --- a/packages/create-ship-app/README.md +++ b/packages/create-ship-app/README.md @@ -10,15 +10,16 @@ The [Ship](https://ship.paralect.com) is a toolkit for makers to **ship** better products faster 🚀. -It is based on several open-source components, resulting from years of hard work [Paralect](https://www.paralect.com) team. We carefully select, document, and share our production-ready knowledge with you. Our technological choices are based on the following main tools: [Next.js](https://nextjs.org/), [React Query](https://react-query.tanstack.com/), [React Hook Form](https://react-hook-form.com/), [Mantine UI](https://mantine.dev/), [Koa.js](https://koajs.com/), [Socket.IO](https://socket.io/), [MongoDB](https://www.mongodb.com/), [Turborepo](https://turbo.build/repo/docs), [Docker](https://www.docker.com/), [Kubernetes](https://kubernetes.io/), [GitHub Actions](https://github.com/features/actions) and [TypeScript](https://www.typescriptlang.org/). +It is based on several open-source components, resulting from years of hard work by the [Paralect](https://www.paralect.com) team. We carefully select, document, and share our production-ready knowledge with you. Our technological choices are based on the following main tools: [Next.js](https://nextjs.org/), [Tanstack Query](https://tanstack.com/query/latest/), [React Hook Form](https://react-hook-form.com/), [Tailwind CSS](https://tailwindcss.com/), [shadcn/ui](https://ui.shadcn.com/), [Koa.js](https://koajs.com/), [Socket.IO](https://socket.io/), [MongoDB](https://www.mongodb.com/), [Turborepo](https://turbo.build/repo/docs), [Docker](https://www.docker.com/), [Kubernetes](https://kubernetes.io/), [GitHub Actions](https://github.com/features/actions) and [TypeScript](https://www.typescriptlang.org/). -We encourage developers to share production-ready solutions and help business ship something people need as quickly as possible. +We encourage developers to share production-ready solutions and help businesses ship something people need as quickly as possible. ## Features - Full-stack boilerplate tested on production projects 🔥 -- Customizable UI 🧱 -- Email/password and Google Oauth authentication 🔐 +- Customizable UI with [shadcn/ui](https://ui.shadcn.com/) and [Tailwind CSS](https://tailwindcss.com/) 🧱 +- Email/password and Google OAuth authentication 🔐 +- Plugin system — install pre-built features with a single command 🔌 - Multiple environments support 📝 - Reactive MongoDB [configuration](https://ship.paralect.com/docs/packages/node-mongo) with CUD events publishing 🍃 - [Kubernetes](https://ship.paralect.com/docs/deployment/kubernetes/overview) and [DO Apps](https://ship.paralect.com/docs/deployment/digital-ocean-apps) deployment for AWS and Digital Ocean platforms ☁ @@ -38,21 +39,38 @@ We encourage developers to share production-ready solutions and help business sh npx create-ship-app@latest init ``` -## [Documentation](https://ship.paralect.com/docs/intro) +## Plugins + +Extend your project with pre-built features using the plugin system: + +```shell +npx create-ship-app@latest install +``` + +Available plugins: + +| Plugin | Description | +|---|---| +| `stripe-subscriptions` | Subscription billing with Stripe Checkout, webhooks, and a pricing page | +| `ai-chat` | AI chat with streaming responses powered by Google Gemini | + +See the [Plugins documentation](https://ship.paralect.com/docs/plugins/overview) for details. + +## [Documentation](https://ship.paralect.com/docs/introduction) ## Why Ship? -Shipping is crucial part of any new product. The quicker you ship, the more time you have to validate your hypotheses. The quicker you validate your idea, the sooner you know if you're building what people want. +Shipping is a crucial part of any new product. The quicker you ship, the more time you have to validate your hypotheses. The quicker you validate your idea, the sooner you know if you're building what people want. -In the rush, developers often ignore quality. Backups, monitoring, proper data validation and many other things seems not so important in the beginning. Sometimes you're lucky, sometimes not. +In the rush, developers often ignore quality. Backups, monitoring, proper data validation and many other things seem not so important in the beginning. Sometimes you're lucky, sometimes not. -We believe we could ship great products faster, while maintain decent quality and have a plan for scaling when hypotheses where right. +We believe we could ship great products faster, while maintaining decent quality and have a plan for scaling when hypotheses are right. ## Core concepts -- We automatically build ship out of the number of smaller components. Our ultimate goal is too keep only parts your need for your product development. -- Every component is kept is tiny as possible to simplify maintenance and stay up to date with new releases. -- Ship is always in a production-ready state. We test every release manually to make sure great developer experience. We use ship to build our products, see more [here](https://www.paralect.com/build-stage). +- We automatically build Ship out of a number of smaller components. Our ultimate goal is to keep only the parts you need for your product development. +- Every component is kept as tiny as possible to simplify maintenance and stay up to date with new releases. +- Ship is always in a production-ready state. We test every release manually to make sure of a great developer experience. We use Ship to build our products, see more [here](https://www.paralect.com/build-stage). ## License diff --git a/template/README.md b/template/README.md index cde0aef16..492e06430 100644 --- a/template/README.md +++ b/template/README.md @@ -1,4 +1,19 @@ -# [Documentation](https://ship.paralect.com/docs/introduction) +# Ship + +> Full-Stack Node.js boilerplate — [Next.js](https://nextjs.org/) (Pages Router) + [Koa.js](https://koajs.com/) + [MongoDB](https://www.mongodb.com/) + [TypeScript](https://www.typescriptlang.org/) + +## [Documentation](https://ship.paralect.com/docs/introduction) + +## Tech Stack + +| Layer | Technologies | +|---|---| +| **Web** | Next.js (Pages Router), Tanstack Query, React Hook Form, Tailwind CSS, shadcn/ui | +| **API** | Koa.js, Zod validation, auto-discovered endpoints | +| **Database** | MongoDB with [@paralect/node-mongo](https://ship.paralect.com/docs/packages/node-mongo) | +| **Shared** | Auto-generated typed API client, Zod schemas | +| **Infra** | Docker, Turborepo, Redis, GitHub Actions | +| **Deployment** | Digital Ocean Apps, Render, Kubernetes (DO / AWS EKS) | ## Prerequisites @@ -57,3 +72,13 @@ pnpm run docker ``` You can also run infrastructure services separately using the `./bin/start.sh` bash script. + +## Plugins + +Extend your project with pre-built features: + +```sh +npx create-ship-app@latest install +``` + +Available plugins: `stripe-subscriptions`, `ai-chat`. See [Plugins docs](https://ship.paralect.com/docs/plugins/overview). From e95180c78592f6107209ed831664b44a492a6dc6 Mon Sep 17 00:00:00 2001 From: Alexey Kasperovich Date: Wed, 11 Mar 2026 16:59:29 +0300 Subject: [PATCH 5/7] fix: linter action content-collection install --- .github/workflows/linter.template.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/linter.template.yml b/.github/workflows/linter.template.yml index 959e033b9..f755456d0 100644 --- a/.github/workflows/linter.template.yml +++ b/.github/workflows/linter.template.yml @@ -41,6 +41,10 @@ jobs: - name: Install Dependencies run: cd template && pnpm install + - name: Generate Content Collections + if: inputs.component == 'template/web' + run: cd template && pnpm --filter web content-collections build + - name: Run Linters uses: wearerequired/lint-action@v2 with: From ccdbd4a08be73275252ede602006655e727e2586 Mon Sep 17 00:00:00 2001 From: Alexey Kasperovich Date: Wed, 11 Mar 2026 17:04:19 +0300 Subject: [PATCH 6/7] fix: linter --- .github/workflows/linter.template.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/linter.template.yml b/.github/workflows/linter.template.yml index f755456d0..d39c607df 100644 --- a/.github/workflows/linter.template.yml +++ b/.github/workflows/linter.template.yml @@ -43,7 +43,7 @@ jobs: - name: Generate Content Collections if: inputs.component == 'template/web' - run: cd template && pnpm --filter web content-collections build + run: cd template && pnpm --filter web exec content-collections build - name: Run Linters uses: wearerequired/lint-action@v2 From cd0880e069034310a021a31c662bf6e4a8602a82 Mon Sep 17 00:00:00 2001 From: Alexey Kasperovich Date: Wed, 11 Mar 2026 17:18:58 +0300 Subject: [PATCH 7/7] fix: linter --- .github/workflows/linter.template.yml | 2 +- template/apps/web/package.json | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/linter.template.yml b/.github/workflows/linter.template.yml index d39c607df..c4c0470dc 100644 --- a/.github/workflows/linter.template.yml +++ b/.github/workflows/linter.template.yml @@ -43,7 +43,7 @@ jobs: - name: Generate Content Collections if: inputs.component == 'template/web' - run: cd template && pnpm --filter web exec content-collections build + run: cd template && pnpm --filter web run content-collections:build - name: Run Linters uses: wearerequired/lint-action@v2 diff --git a/template/apps/web/package.json b/template/apps/web/package.json index 6adf4dfcf..93adb2d5b 100644 --- a/template/apps/web/package.json +++ b/template/apps/web/package.json @@ -10,6 +10,7 @@ "dev": "next dev -p 3002", "start": "next start -p 3002", "tsc": "tsc --noEmit --watch", + "content-collections:build": "node --input-type=module -e \"const{createBuilder}=await import('@content-collections/core');const b=await createBuilder('./content-collections.ts');await b.build();\"", "prettier": "prettier . --write --config .prettierrc.json", "eslint": "eslint . --fix", "precommit": "lint-staged"