From 91f9bd2357088abc61e953ec3936f747bb68a4d7 Mon Sep 17 00:00:00 2001 From: timoconnellaus Date: Fri, 25 Jul 2025 14:57:20 +1000 Subject: [PATCH 1/2] Feature: replace routes and integrations with customProperties --- .../add-ons/feather-icons/info.json | 22 +++--- .../base/src/components/header.tsx.ejs | 6 +- frameworks/react-cra/add-ons/clerk/info.json | 46 ++++++------ frameworks/react-cra/add-ons/convex/info.json | 34 +++++---- frameworks/react-cra/add-ons/form/info.json | 37 +++++----- frameworks/react-cra/add-ons/neon/info.json | 24 ++++--- frameworks/react-cra/add-ons/sentry/info.json | 24 ++++--- frameworks/react-cra/add-ons/start/info.json | 38 +++++----- frameworks/react-cra/add-ons/store/info.json | 23 +++--- frameworks/react-cra/add-ons/table/info.json | 23 +++--- .../add-ons/tanstack-query/info.json | 42 +++++------ .../base/src/components/Header.tsx.ejs | 7 +- frameworks/react-cra/src/index.ts | 72 ++++++++++++++++--- frameworks/solid/add-ons/form/info.json | 23 +++--- frameworks/solid/add-ons/sentry/info.json | 22 +++--- frameworks/solid/add-ons/start/info.json | 22 +++--- frameworks/solid/add-ons/store/info.json | 23 +++--- .../solid/add-ons/tanstack-query/info.json | 46 ++++++------ .../base/src/components/Header.tsx.ejs | 5 +- frameworks/solid/src/index.ts | 47 +++++++++--- .../cta-engine/src/custom-add-ons/add-on.ts | 16 +++-- .../cta-engine/src/custom-add-ons/starter.ts | 1 - packages/cta-engine/src/frameworks.ts | 26 ++++++- packages/cta-engine/src/index.ts | 1 - packages/cta-engine/src/template-file.ts | 38 ++++++---- packages/cta-engine/src/types.ts | 24 ++----- .../cta-engine/tests/template-file.test.ts | 34 +++++---- 27 files changed, 455 insertions(+), 271 deletions(-) diff --git a/examples/custom-cli/create-qwik-app/add-ons/feather-icons/info.json b/examples/custom-cli/create-qwik-app/add-ons/feather-icons/info.json index 58c71fe1..471f0fcf 100644 --- a/examples/custom-cli/create-qwik-app/add-ons/feather-icons/info.json +++ b/examples/custom-cli/create-qwik-app/add-ons/feather-icons/info.json @@ -2,15 +2,19 @@ "name": "Feather Icons", "description": "Add Feather Icons to your application.", "phase": "add-on", - "modes": ["default"], + "modes": [ + "default" + ], "type": "add-on", "link": "https://github.com/egmaleta/qwik-feather-icons", - "routes": [ - { - "url": "/demo-feather", - "name": "Feather Icons", - "path": "src/routes/demo-feather/index.tsx", - "jsName": "FeatherIconsDemo" - } - ] + "customProperties": { + "routes": [ + { + "url": "/demo-feather", + "name": "Feather Icons", + "path": "src/routes/demo-feather/index.tsx", + "jsName": "FeatherIconsDemo" + } + ] + } } diff --git a/examples/custom-cli/create-qwik-app/project/base/src/components/header.tsx.ejs b/examples/custom-cli/create-qwik-app/project/base/src/components/header.tsx.ejs index 29b690a3..bbfe3cb1 100644 --- a/examples/custom-cli/create-qwik-app/project/base/src/components/header.tsx.ejs +++ b/examples/custom-cli/create-qwik-app/project/base/src/components/header.tsx.ejs @@ -12,9 +12,9 @@ export default component$(() => {
Home
-<% for(const addOn of addOns) { for(const route of (addOn?.routes||[])?.filter(r => r.url && r.name)) { %> -
<%= route.name %>
-<% } } %> +<% for(const route of (routes||[]).filter(r => r.url && r.name)) { %> +
<%= route.name %>
+ <% } %> <% if (integrations.filter(i => i.type === 'header-user').length > 0) { %>
diff --git a/frameworks/react-cra/add-ons/clerk/info.json b/frameworks/react-cra/add-ons/clerk/info.json index 1e8a647f..2fe5a353 100644 --- a/frameworks/react-cra/add-ons/clerk/info.json +++ b/frameworks/react-cra/add-ons/clerk/info.json @@ -2,27 +2,31 @@ "name": "Clerk", "description": "Add Clerk authentication to your application.", "phase": "add-on", - "modes": ["file-router"], + "modes": [ + "file-router" + ], "type": "add-on", "link": "https://clerk.com", - "routes": [ - { - "url": "/demo/clerk", - "name": "Clerk", - "path": "src/routes/demo.clerk.tsx", - "jsName": "ClerkDemo" - } - ], - "integrations": [ - { - "type": "header-user", - "jsName": "ClerkHeader", - "path": "src/integrations/clerk/header-user.tsx" - }, - { - "type": "provider", - "jsName": "ClerkProvider", - "path": "src/integrations/clerk/provider.tsx" - } - ] + "customProperties": { + "routes": [ + { + "url": "/demo/clerk", + "name": "Clerk", + "path": "src/routes/demo.clerk.tsx", + "jsName": "ClerkDemo" + } + ], + "integrations": [ + { + "type": "header-user", + "jsName": "ClerkHeader", + "path": "src/integrations/clerk/header-user.tsx" + }, + { + "type": "provider", + "jsName": "ClerkProvider", + "path": "src/integrations/clerk/provider.tsx" + } + ] + } } diff --git a/frameworks/react-cra/add-ons/convex/info.json b/frameworks/react-cra/add-ons/convex/info.json index 75a3443c..1af89d05 100644 --- a/frameworks/react-cra/add-ons/convex/info.json +++ b/frameworks/react-cra/add-ons/convex/info.json @@ -4,20 +4,24 @@ "link": "https://convex.dev", "phase": "add-on", "type": "add-on", - "modes": ["file-router"], - "routes": [ - { - "url": "/demo/convex", - "name": "Convex", - "path": "src/routes/demo.convex.tsx", - "jsName": "ConvexDemo" - } + "modes": [ + "file-router" ], - "integrations": [ - { - "type": "provider", - "path": "src/integrations/convex/provider.tsx", - "jsName": "ConvexProvider" - } - ] + "customProperties": { + "routes": [ + { + "url": "/demo/convex", + "name": "Convex", + "path": "src/routes/demo.convex.tsx", + "jsName": "ConvexDemo" + } + ], + "integrations": [ + { + "type": "provider", + "path": "src/integrations/convex/provider.tsx", + "jsName": "ConvexProvider" + } + ] + } } diff --git a/frameworks/react-cra/add-ons/form/info.json b/frameworks/react-cra/add-ons/form/info.json index d06cb505..9748f693 100644 --- a/frameworks/react-cra/add-ons/form/info.json +++ b/frameworks/react-cra/add-ons/form/info.json @@ -3,22 +3,11 @@ "description": "TanStack Form", "phase": "add-on", "type": "add-on", - "modes": ["file-router", "code-router"], - "link": "https://tanstack.com/form/latest", - "routes": [ - { - "url": "/demo/form/simple", - "name": "Simple Form", - "path": "src/routes/demo.form.simple.tsx", - "jsName": "FormSimpleDemo" - }, - { - "url": "/demo/form/address", - "name": "Address Form", - "path": "src/routes/demo.form.address.tsx", - "jsName": "FormAddressDemo" - } + "modes": [ + "file-router", + "code-router" ], + "link": "https://tanstack.com/form/latest", "shadcnComponents": [ "button", "select", @@ -27,5 +16,21 @@ "slider", "switch", "label" - ] + ], + "customProperties": { + "routes": [ + { + "url": "/demo/form/simple", + "name": "Simple Form", + "path": "src/routes/demo.form.simple.tsx", + "jsName": "FormSimpleDemo" + }, + { + "url": "/demo/form/address", + "name": "Address Form", + "path": "src/routes/demo.form.address.tsx", + "jsName": "FormAddressDemo" + } + ] + } } diff --git a/frameworks/react-cra/add-ons/neon/info.json b/frameworks/react-cra/add-ons/neon/info.json index 4ddc43ab..b6ea638d 100644 --- a/frameworks/react-cra/add-ons/neon/info.json +++ b/frameworks/react-cra/add-ons/neon/info.json @@ -4,14 +4,20 @@ "link": "https://neon.com", "phase": "add-on", "type": "add-on", - "modes": ["file-router"], - "routes": [ - { - "url": "/demo/neon", - "name": "Neon", - "path": "src/routes/demo.neon.tsx", - "jsName": "NeonDemo" - } + "modes": [ + "file-router" ], - "dependsOn": ["start"] + "dependsOn": [ + "start" + ], + "customProperties": { + "routes": [ + { + "url": "/demo/neon", + "name": "Neon", + "path": "src/routes/demo.neon.tsx", + "jsName": "NeonDemo" + } + ] + } } diff --git a/frameworks/react-cra/add-ons/sentry/info.json b/frameworks/react-cra/add-ons/sentry/info.json index add3e6e8..22c98423 100644 --- a/frameworks/react-cra/add-ons/sentry/info.json +++ b/frameworks/react-cra/add-ons/sentry/info.json @@ -3,15 +3,21 @@ "phase": "setup", "description": "Add Sentry for error monitoring, tracing, and session replays (requires Start).", "link": "https://sentry.com/", - "modes": ["file-router"], + "modes": [ + "file-router" + ], "type": "add-on", - "routes": [ - { - "url": "/demo/sentry/testing", - "name": "Sentry", - "path": "src/routes/demo.sentry.testing.tsx", - "jsName": "SentryDemo" - } + "dependsOn": [ + "start" ], - "dependsOn": ["start"] + "customProperties": { + "routes": [ + { + "url": "/demo/sentry/testing", + "name": "Sentry", + "path": "src/routes/demo.sentry.testing.tsx", + "jsName": "SentryDemo" + } + ] + } } diff --git a/frameworks/react-cra/add-ons/start/info.json b/frameworks/react-cra/add-ons/start/info.json index b86e365d..a8cd9f13 100644 --- a/frameworks/react-cra/add-ons/start/info.json +++ b/frameworks/react-cra/add-ons/start/info.json @@ -3,27 +3,33 @@ "phase": "setup", "description": "Add TanStack Start for SSR, API endpoints, and more.", "link": "https://tanstack.com/start/latest", - "modes": ["file-router"], + "modes": [ + "file-router" + ], "type": "add-on", "warning": "TanStack Start is not yet at 1.0 and may change significantly or not be compatible with other add-ons.\nMigrating to Start might require deleting node_modules and re-installing.", - "routes": [ - { - "url": "/demo/start/server-funcs", - "name": "Start - Server Functions", - "path": "src/routes/demo.start.server-funcs.tsx", - "jsName": "StartServerFuncsDemo" - }, - { - "url": "/demo/start/api-request", - "name": "Start - API Request", - "path": "src/routes/demo.start.api-request.tsx", - "jsName": "StartApiRequestDemo" - } - ], "deletedFiles": [ "./index.html", "./src/main.tsx", "./src/App.css" ], - "addOnSpecialSteps": ["rimraf-node-modules"] + "addOnSpecialSteps": [ + "rimraf-node-modules" + ], + "customProperties": { + "routes": [ + { + "url": "/demo/start/server-funcs", + "name": "Start - Server Functions", + "path": "src/routes/demo.start.server-funcs.tsx", + "jsName": "StartServerFuncsDemo" + }, + { + "url": "/demo/start/api-request", + "name": "Start - API Request", + "path": "src/routes/demo.start.api-request.tsx", + "jsName": "StartApiRequestDemo" + } + ] + } } diff --git a/frameworks/react-cra/add-ons/store/info.json b/frameworks/react-cra/add-ons/store/info.json index 27bac2fd..5c231a2f 100644 --- a/frameworks/react-cra/add-ons/store/info.json +++ b/frameworks/react-cra/add-ons/store/info.json @@ -4,13 +4,18 @@ "phase": "add-on", "link": "https://tanstack.com/store/latest", "type": "add-on", - "modes": ["file-router", "code-router"], - "routes": [ - { - "url": "/demo/store", - "name": "Store", - "path": "src/routes/demo.store.tsx", - "jsName": "StoreDemo" - } - ] + "modes": [ + "file-router", + "code-router" + ], + "customProperties": { + "routes": [ + { + "url": "/demo/store", + "name": "Store", + "path": "src/routes/demo.store.tsx", + "jsName": "StoreDemo" + } + ] + } } diff --git a/frameworks/react-cra/add-ons/table/info.json b/frameworks/react-cra/add-ons/table/info.json index 6dc0ca3e..59d9fac8 100644 --- a/frameworks/react-cra/add-ons/table/info.json +++ b/frameworks/react-cra/add-ons/table/info.json @@ -2,15 +2,20 @@ "name": "Table", "description": "Integrate TanStack Table into your application.", "phase": "add-on", - "modes": ["file-router", "code-router"], + "modes": [ + "file-router", + "code-router" + ], "link": "https://tanstack.com/table/latest", "type": "add-on", - "routes": [ - { - "url": "/demo/table", - "name": "TanStack Table", - "path": "src/routes/demo.table.tsx", - "jsName": "TableDemo" - } - ] + "customProperties": { + "routes": [ + { + "url": "/demo/table", + "name": "TanStack Table", + "path": "src/routes/demo.table.tsx", + "jsName": "TableDemo" + } + ] + } } diff --git a/frameworks/react-cra/add-ons/tanstack-query/info.json b/frameworks/react-cra/add-ons/tanstack-query/info.json index 0f3d9d9e..1fdbd1a4 100644 --- a/frameworks/react-cra/add-ons/tanstack-query/info.json +++ b/frameworks/react-cra/add-ons/tanstack-query/info.json @@ -5,24 +5,26 @@ "modes": ["file-router", "code-router"], "type": "add-on", "link": "https://tanstack.com/query/latest", - "routes": [ - { - "url": "/demo/tanstack-query", - "name": "TanStack Query", - "path": "src/routes/demo.tanstack-query.tsx", - "jsName": "TanStackQueryDemo" - } - ], - "integrations": [ - { - "type": "root-provider", - "path": "src/integrations/tanstack-query/root-provider.tsx", - "jsName": "TanStackQueryProvider" - }, - { - "type": "layout", - "path": "src/integrations/tanstack-query/layout.tsx", - "jsName": "TanStackQueryLayout" - } - ] + "customProperties": { + "routes": [ + { + "url": "/demo/tanstack-query", + "name": "TanStack Query", + "path": "src/routes/demo.tanstack-query.tsx", + "jsName": "TanStackQueryDemo" + } + ], + "integrations": [ + { + "type": "root-provider", + "path": "src/integrations/tanstack-query/root-provider.tsx", + "jsName": "TanStackQueryProvider" + }, + { + "type": "layout", + "path": "src/integrations/tanstack-query/layout.tsx", + "jsName": "TanStackQueryLayout" + } + ] + } } diff --git a/frameworks/react-cra/project/base/src/components/Header.tsx.ejs b/frameworks/react-cra/project/base/src/components/Header.tsx.ejs index f9e9d1cd..b4e927c5 100644 --- a/frameworks/react-cra/project/base/src/components/Header.tsx.ejs +++ b/frameworks/react-cra/project/base/src/components/Header.tsx.ejs @@ -10,10 +10,9 @@ export default function Header() {
Home
-<% for(const addOn of addOns) { - for(const route of (addOn?.routes||[])?.filter(r => r.url && r.name)) { %> -
<%= route.name %>
-<% } } %> +<% for(const route of (routes||[]).filter(r => r.url && r.name)) { %> +
<%= route.name %>
+ <% } %> <% if (integrations.filter(i => i.type === 'header-user').length > 0) { %>
diff --git a/frameworks/react-cra/src/index.ts b/frameworks/react-cra/src/index.ts index 6297064d..1cd240db 100644 --- a/frameworks/react-cra/src/index.ts +++ b/frameworks/react-cra/src/index.ts @@ -1,32 +1,84 @@ import { dirname, join } from 'node:path' import { fileURLToPath } from 'node:url' - +import { z } from 'zod' import { registerFramework, scanAddOnDirectories, scanProjectDirectory, } from '@tanstack/cta-engine' -import type { FrameworkDefinition } from '@tanstack/cta-engine' +import type { ZodTypeAny } from 'zod' -export function createFrameworkDefinition(): FrameworkDefinition { +export function createFrameworkDefinition(): any { const baseDirectory = dirname(dirname(fileURLToPath(import.meta.url))) - const addOns = scanAddOnDirectories([ - join(baseDirectory, 'add-ons'), - join(baseDirectory, 'toolchains'), - join(baseDirectory, 'examples'), - ]) + // Define custom properties for React framework + const customProperties = { + routes: z + .array( + z.object({ + url: z.string().optional(), + name: z.string().optional(), + path: z.string(), + jsName: z.string(), + }), + ) + .optional(), + integrations: z + .array( + z.object({ + type: z.enum(['provider', 'root-provider', 'layout', 'header-user']), + path: z.string(), + jsName: z.string(), + }), + ) + .optional(), + } as Record + + const addOns = scanAddOnDirectories( + [ + join(baseDirectory, 'add-ons'), + join(baseDirectory, 'toolchains'), + join(baseDirectory, 'examples'), + ], + { customProperties }, + ) const { files, basePackageJSON, optionalPackages } = scanProjectDirectory( join(baseDirectory, 'project'), join(baseDirectory, 'project/base'), ) - return { + const framework = { id: 'react-cra', name: 'React', description: 'Templates for React CRA', version: '0.1.0', + customProperties: { + routes: z + .array( + z.object({ + url: z.string().optional(), + name: z.string().optional(), + path: z.string(), + jsName: z.string(), + }), + ) + .optional(), + integrations: z + .array( + z.object({ + type: z.enum([ + 'provider', + 'root-provider', + 'layout', + 'header-user', + ]), + path: z.string(), + jsName: z.string(), + }), + ) + .optional(), + }, base: files, addOns, basePackageJSON, @@ -44,6 +96,8 @@ export function createFrameworkDefinition(): FrameworkDefinition { }, }, } + + return framework } export function register() { diff --git a/frameworks/solid/add-ons/form/info.json b/frameworks/solid/add-ons/form/info.json index 78b9389d..25969429 100644 --- a/frameworks/solid/add-ons/form/info.json +++ b/frameworks/solid/add-ons/form/info.json @@ -3,14 +3,19 @@ "description": "TanStack Form", "phase": "add-on", "link": "https://tanstack.com/form/latest", - "modes": ["file-router", "code-router"], + "modes": [ + "file-router", + "code-router" + ], "type": "add-on", - "routes": [ - { - "url": "/demo/form", - "name": "Form", - "path": "src/routes/demo.form.tsx", - "jsName": "FormDemo" - } - ] + "customProperties": { + "routes": [ + { + "url": "/demo/form", + "name": "Form", + "path": "src/routes/demo.form.tsx", + "jsName": "FormDemo" + } + ] + } } diff --git a/frameworks/solid/add-ons/sentry/info.json b/frameworks/solid/add-ons/sentry/info.json index 92cda812..0656dc34 100644 --- a/frameworks/solid/add-ons/sentry/info.json +++ b/frameworks/solid/add-ons/sentry/info.json @@ -3,14 +3,18 @@ "phase": "setup", "description": "Add Sentry for error monitoring and crash reporting (requires Start).", "link": "https://sentry.com/", - "modes": ["file-router"], + "modes": [ + "file-router" + ], "type": "add-on", - "routes": [ - { - "url": "/demo/sentry/bad-event-handler", - "name": "Sentry", - "path": "src/routes/demo.sentry.bad-event-handler.tsx", - "jsName": "SentryBadEventHandlerDemo" - } - ] + "customProperties": { + "routes": [ + { + "url": "/demo/sentry/bad-event-handler", + "name": "Sentry", + "path": "src/routes/demo.sentry.bad-event-handler.tsx", + "jsName": "SentryBadEventHandlerDemo" + } + ] + } } diff --git a/frameworks/solid/add-ons/start/info.json b/frameworks/solid/add-ons/start/info.json index 94ba8ada..0f346c91 100644 --- a/frameworks/solid/add-ons/start/info.json +++ b/frameworks/solid/add-ons/start/info.json @@ -3,15 +3,19 @@ "phase": "setup", "description": "Add TanStack Start for SSR, API endpoints, and more.", "link": "https://tanstack.com/start/latest", - "modes": ["file-router"], + "modes": [ + "file-router" + ], "type": "add-on", "warning": "TanStack Start is not yet at 1.0 and may change significantly or not be compatible with other add-ons.", - "routes": [ - { - "url": "/demo/start/server-funcs", - "name": "Start - Server Functions", - "path": "src/routes/demo.start.server-funcs.tsx", - "jsName": "StartServerFuncsDemo" - } - ] + "customProperties": { + "routes": [ + { + "url": "/demo/start/server-funcs", + "name": "Start - Server Functions", + "path": "src/routes/demo.start.server-funcs.tsx", + "jsName": "StartServerFuncsDemo" + } + ] + } } diff --git a/frameworks/solid/add-ons/store/info.json b/frameworks/solid/add-ons/store/info.json index faac463e..a29997f2 100644 --- a/frameworks/solid/add-ons/store/info.json +++ b/frameworks/solid/add-ons/store/info.json @@ -2,15 +2,20 @@ "name": "Store", "description": "Add TanStack Store to your application.", "phase": "add-on", - "modes": ["file-router", "code-router"], + "modes": [ + "file-router", + "code-router" + ], "type": "add-on", "link": "https://tanstack.com/store/latest", - "routes": [ - { - "url": "/demo/store", - "name": "Store", - "path": "src/routes/demo.store.tsx", - "jsName": "StoreDemo" - } - ] + "customProperties": { + "routes": [ + { + "url": "/demo/store", + "name": "Store", + "path": "src/routes/demo.store.tsx", + "jsName": "StoreDemo" + } + ] + } } diff --git a/frameworks/solid/add-ons/tanstack-query/info.json b/frameworks/solid/add-ons/tanstack-query/info.json index 23017a9e..798ccbb3 100644 --- a/frameworks/solid/add-ons/tanstack-query/info.json +++ b/frameworks/solid/add-ons/tanstack-query/info.json @@ -2,27 +2,31 @@ "name": "TanStack Query", "description": "Integrate TanStack Query into your application.", "phase": "add-on", - "modes": ["file-router"], + "modes": [ + "file-router" + ], "type": "add-on", "link": "https://tanstack.com/query/latest", - "routes": [ - { - "url": "/demo/tanstack-query", - "name": "TanStack Query", - "path": "src/routes/demo.tanstack-query.tsx", - "jsName": "TanStackQueryDemo" - } - ], - "integrations": [ - { - "type": "provider", - "path": "src/integrations/tanstack-query/provider.tsx", - "jsName": "TanStackQueryProvider" - }, - { - "type": "header-user", - "path": "src/integrations/tanstack-query/header-user.tsx", - "jsName": "TanStackQueryHeaderUser" - } - ] + "customProperties": { + "routes": [ + { + "url": "/demo/tanstack-query", + "name": "TanStack Query", + "path": "src/routes/demo.tanstack-query.tsx", + "jsName": "TanStackQueryDemo" + } + ], + "integrations": [ + { + "type": "provider", + "path": "src/integrations/tanstack-query/provider.tsx", + "jsName": "TanStackQueryProvider" + }, + { + "type": "header-user", + "path": "src/integrations/tanstack-query/header-user.tsx", + "jsName": "TanStackQueryHeaderUser" + } + ] + } } diff --git a/frameworks/solid/project/base/src/components/Header.tsx.ejs b/frameworks/solid/project/base/src/components/Header.tsx.ejs index 1fbe677a..58643837 100644 --- a/frameworks/solid/project/base/src/components/Header.tsx.ejs +++ b/frameworks/solid/project/base/src/components/Header.tsx.ejs @@ -10,10 +10,9 @@ export default function Header() {
Home
- <% for(const addOn of addOns) { - for(const route of (addOn?.routes||[])?.filter(r => r.url && r.name) || []) { %> + <% for(const route of (routes||[]).filter(r => r.url && r.name)) { %>
<%= route.name %>
- <% } } %> + <% } %>
diff --git a/frameworks/solid/src/index.ts b/frameworks/solid/src/index.ts index 11d839b1..2e4f8beb 100644 --- a/frameworks/solid/src/index.ts +++ b/frameworks/solid/src/index.ts @@ -1,32 +1,59 @@ import { dirname, join } from 'node:path' import { fileURLToPath } from 'node:url' - +import { z } from 'zod' import { registerFramework, scanAddOnDirectories, scanProjectDirectory, } from '@tanstack/cta-engine' -import type { FrameworkDefinition } from '@tanstack/cta-engine' +import type { ZodTypeAny } from 'zod' -export function createFrameworkDefinition(): FrameworkDefinition { +export function createFrameworkDefinition(): any { const baseDirectory = dirname(dirname(fileURLToPath(import.meta.url))) - const addOns = scanAddOnDirectories([ - join(baseDirectory, 'add-ons'), - join(baseDirectory, 'toolchains'), - join(baseDirectory, 'examples'), - ]) + // Define custom properties for Solid framework + const customProperties = { + routes: z + .array( + z.object({ + url: z.string().optional(), + name: z.string().optional(), + path: z.string(), + jsName: z.string(), + }), + ) + .optional(), + integrations: z + .array( + z.object({ + type: z.enum(['provider', 'root-provider', 'layout', 'header-user']), + path: z.string(), + jsName: z.string(), + }), + ) + .optional(), + } as Record + + const addOns = scanAddOnDirectories( + [ + join(baseDirectory, 'add-ons'), + join(baseDirectory, 'toolchains'), + join(baseDirectory, 'examples'), + ], + { customProperties }, + ) const { files, basePackageJSON, optionalPackages } = scanProjectDirectory( join(baseDirectory, 'project'), join(baseDirectory, 'project/base'), ) - return { + const framework = { id: 'solid', name: 'Solid', description: 'Solid templates for Tanstack Router Applications', version: '0.1.0', + customProperties, base: files, addOns, basePackageJSON, @@ -44,6 +71,8 @@ export function createFrameworkDefinition(): FrameworkDefinition { }, }, } + + return framework } export function register() { diff --git a/packages/cta-engine/src/custom-add-ons/add-on.ts b/packages/cta-engine/src/custom-add-ons/add-on.ts index 8dfb077a..b7698e57 100644 --- a/packages/cta-engine/src/custom-add-ons/add-on.ts +++ b/packages/cta-engine/src/custom-add-ons/add-on.ts @@ -139,7 +139,9 @@ export async function readOrGenerateAddOnInfo( shadcnComponents: [], framework: options.framework, modes: [options.mode], - routes: [], + customProperties: { + routes: [], + }, warning: '', phase: 'add-on', type: 'add-on', @@ -185,9 +187,15 @@ export async function buildAssetsDirectory( }) if (file.includes('/routes/')) { const { url, code, name, jsName } = templatize(changedFiles[file], file) - info.routes ||= [] - if (!info.routes.find((r) => r.url === url)) { - info.routes.push({ + if (!info.customProperties) { + info.customProperties = {} + } + if (!info.customProperties.routes) { + info.customProperties.routes = [] + } + const routes = info.customProperties.routes as Array + if (!routes.find((r: any) => r.url === url)) { + routes.push({ url, name, jsName, diff --git a/packages/cta-engine/src/custom-add-ons/starter.ts b/packages/cta-engine/src/custom-add-ons/starter.ts index 4c193f68..c1e9d3e1 100644 --- a/packages/cta-engine/src/custom-add-ons/starter.ts +++ b/packages/cta-engine/src/custom-add-ons/starter.ts @@ -40,7 +40,6 @@ export async function readOrGenerateStarterInfo( shadcnComponents: [], framework: options.framework, mode: options.mode!, - routes: [], warning: '', type: 'starter', packageAdditions: { diff --git a/packages/cta-engine/src/frameworks.ts b/packages/cta-engine/src/frameworks.ts index ae162797..2c41dc88 100644 --- a/packages/cta-engine/src/frameworks.ts +++ b/packages/cta-engine/src/frameworks.ts @@ -45,7 +45,10 @@ export function scanProjectDirectory( } } -export function scanAddOnDirectories(addOnsDirectories: Array) { +export function scanAddOnDirectories( + addOnsDirectories: Array, + framework?: { customProperties?: Record }, +) { const addOns: Array = [] for (const addOnsBase of addOnsDirectories) { @@ -93,6 +96,26 @@ export function scanAddOnDirectories(addOnsDirectories: Array) { return Promise.resolve(files[path]) } + // Validate custom properties if framework defines them + let validatedCustomProperties: Record | undefined + if (framework?.customProperties && info.customProperties) { + validatedCustomProperties = {} + for (const [key, schema] of Object.entries(framework.customProperties)) { + if (key in info.customProperties) { + try { + validatedCustomProperties[key] = schema.parse(info.customProperties[key]) + } catch (error: any) { + throw new Error( + `Invalid custom property "${key}" in add-on "${dir}": ${error.message}` + ) + } + } + } + } else if (info.customProperties) { + // If no framework validation, pass through as-is + validatedCustomProperties = info.customProperties + } + addOns.push({ ...info, id: dir, @@ -100,6 +123,7 @@ export function scanAddOnDirectories(addOnsDirectories: Array) { readme, files, smallLogo, + customProperties: validatedCustomProperties, getFiles, getFileContents, getDeletedFiles: () => Promise.resolve(info.deletedFiles ?? []), diff --git a/packages/cta-engine/src/index.ts b/packages/cta-engine/src/index.ts index 253ac72e..1d4a3d72 100644 --- a/packages/cta-engine/src/index.ts +++ b/packages/cta-engine/src/index.ts @@ -68,7 +68,6 @@ export { StopEvent, AddOnCompiledSchema, AddOnInfoSchema, - IntegrationSchema, } from './types.js' export type { diff --git a/packages/cta-engine/src/template-file.ts b/packages/cta-engine/src/template-file.ts index 80bfd32c..0de8d38e 100644 --- a/packages/cta-engine/src/template-file.ts +++ b/packages/cta-engine/src/template-file.ts @@ -9,7 +9,7 @@ import { } from './package-manager.js' import { relativePath } from './file-helpers.js' -import type { AddOn, Environment, Options } from './types.js' +import type { Environment, Options } from './types.js' function convertDotFilesAndPaths(path: string) { return path @@ -50,19 +50,32 @@ export function createTemplateFile(environment: Environment, options: Options) { } } - const integrations: Array['integrations'][number]> = [] - for (const addOn of options.chosenAddOns) { - if (addOn.integrations) { - for (const integration of addOn.integrations) { - integrations.push(integration) - } + // Collect all custom properties from add-ons + const customProperties: Record> = {} + + // Initialize arrays for all framework custom properties + if (options.framework.customProperties) { + for (const key of Object.keys(options.framework.customProperties)) { + customProperties[key] = [] } } - const routes: Array['routes'][number]> = [] + // Collect custom properties from each add-on for (const addOn of options.chosenAddOns) { - if (addOn.routes) { - routes.push(...addOn.routes) + if (addOn.customProperties) { + for (const [key, values] of Object.entries(addOn.customProperties)) { + // Initialize array if it doesn't exist (for properties not defined in framework) + if (!(key in customProperties)) { + customProperties[key] = [] + } + + // Add values to the array + if (Array.isArray(values)) { + customProperties[key].push(...values) + } else { + customProperties[key].push(values) + } + } } } @@ -86,8 +99,9 @@ export function createTemplateFile(environment: Environment, options: Options) { codeRouter: options.mode === 'code-router', addOnEnabled, addOns: options.chosenAddOns, - integrations, - routes, + + // Spread custom properties to make them available at top level + ...customProperties, getPackageManagerAddScript, getPackageManagerRunScript, diff --git a/packages/cta-engine/src/types.ts b/packages/cta-engine/src/types.ts index 5f4173f4..d2ab15a9 100644 --- a/packages/cta-engine/src/types.ts +++ b/packages/cta-engine/src/types.ts @@ -1,4 +1,5 @@ import z from 'zod' +import type { ZodTypeAny } from 'zod' import type { PackageManager } from './package-manager.js' @@ -25,16 +26,6 @@ export const AddOnBaseSchema = z.object({ args: z.array(z.string()).optional(), }) .optional(), - routes: z - .array( - z.object({ - url: z.string().optional(), - name: z.string().optional(), - path: z.string(), - jsName: z.string(), - }), - ) - .optional(), packageAdditions: z .object({ dependencies: z.record(z.string(), z.string()).optional(), @@ -63,17 +54,11 @@ export const StarterCompiledSchema = StarterSchema.extend({ deletedFiles: z.array(z.string()), }) -export const IntegrationSchema = z.object({ - type: z.string(), - path: z.string(), - jsName: z.string(), -}) - export const AddOnInfoSchema = AddOnBaseSchema.extend({ modes: z.array(z.string()), - integrations: z.array(IntegrationSchema).optional(), phase: z.enum(['setup', 'add-on']), readme: z.string().optional(), + customProperties: z.record(z.string(), z.unknown()).optional(), }) export const AddOnCompiledSchema = AddOnInfoSchema.extend({ @@ -81,8 +66,6 @@ export const AddOnCompiledSchema = AddOnInfoSchema.extend({ deletedFiles: z.array(z.string()), }) -export type Integration = z.infer - export type AddOnBase = z.infer export type StarterInfo = z.infer @@ -109,6 +92,8 @@ export type FrameworkDefinition = { description: string version: string + customProperties?: Record + base: Record addOns: Array basePackageJSON: Record @@ -127,6 +112,7 @@ export type FrameworkDefinition = { export type Framework = Omit & FileBundleHandler & { getAddOns: () => Array + customProperties?: Record } export interface Options { diff --git a/packages/cta-engine/tests/template-file.test.ts b/packages/cta-engine/tests/template-file.test.ts index 4887867c..6a3e7ed0 100644 --- a/packages/cta-engine/tests/template-file.test.ts +++ b/packages/cta-engine/tests/template-file.test.ts @@ -102,14 +102,16 @@ describe('createTemplateFile', () => { { id: 'test', name: 'Test', - routes: [ - { - path: '/test', - name: 'Test', - url: '/test', - jsName: 'test', - }, - ], + customProperties: { + routes: [ + { + path: '/test', + name: 'Test', + url: '/test', + jsName: 'test', + }, + ], + }, } as AddOn, ], }) @@ -132,13 +134,15 @@ describe('createTemplateFile', () => { { id: 'test', name: 'Test', - integrations: [ - { - type: 'header-user', - path: '/test', - jsName: 'test', - } as Integration, - ], + customProperties: { + integrations: [ + { + type: 'header-user', + path: '/test', + jsName: 'test', + } as Integration, + ], + }, } as AddOn, ], }) From bef58830af23cc9c413a0c5419bf8e43e85090f6 Mon Sep 17 00:00:00 2001 From: timoconnellaus Date: Fri, 25 Jul 2025 16:11:43 +1000 Subject: [PATCH 2/2] refactor: move routes and integrations to root level in info.json files - Keep customProperties schema definition in framework files for validation - Move routes and integrations to root level in all add-on info.json files - Update template processing to collect from root level but validate against customProperties - Fix missing path and jsName in Solid tanchat example - Update add-on generator to create routes at root level This makes routes and integrations feel like first-class properties to developers while maintaining clean schema definitions in the framework. --- .claude/settings.local.json | 9 + CLAUDE.md | 133 +++++ CUSTOM_PROPERTIES_DESIGN.md | 467 ++++++++++++++++++ .../add-ons/feather-icons/info.json | 18 +- frameworks/react-cra/add-ons/clerk/info.json | 42 +- frameworks/react-cra/add-ons/convex/info.json | 32 +- frameworks/react-cra/add-ons/form/info.json | 30 +- frameworks/react-cra/add-ons/neon/info.json | 18 +- frameworks/react-cra/add-ons/sentry/info.json | 18 +- frameworks/react-cra/add-ons/start/info.json | 30 +- frameworks/react-cra/add-ons/store/info.json | 18 +- frameworks/react-cra/add-ons/table/info.json | 18 +- .../add-ons/tanstack-query/info.json | 42 +- frameworks/solid/add-ons/form/info.json | 19 +- frameworks/solid/add-ons/sentry/info.json | 19 +- frameworks/solid/add-ons/start/info.json | 19 +- frameworks/solid/add-ons/store/info.json | 19 +- .../solid/add-ons/tanstack-query/info.json | 43 +- frameworks/solid/examples/tanchat/info.json | 4 +- .../cta-engine/ROUTES_AND_INTEGRATIONS.md | 448 +++++++++++++++++ .../cta-engine/src/custom-add-ons/add-on.ts | 13 +- packages/cta-engine/src/frameworks.ts | 31 ++ packages/cta-engine/src/template-file.ts | 20 + packages/cta-engine/src/types.ts | 19 + 24 files changed, 1314 insertions(+), 215 deletions(-) create mode 100644 .claude/settings.local.json create mode 100644 CLAUDE.md create mode 100644 CUSTOM_PROPERTIES_DESIGN.md create mode 100644 packages/cta-engine/ROUTES_AND_INTEGRATIONS.md diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 00000000..56d162a3 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,9 @@ +{ + "permissions": { + "allow": [ + "Bash(pnpm build:*)", + "Bash(find:*)" + ], + "deny": [] + } +} \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..eb751171 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,133 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Commands + +### Build Commands +```bash +# Build all packages (runs in parallel) +pnpm build + +# Run development mode with watch (builds everything in watch mode) +pnpm dev + +# Clean all node_modules +pnpm cleanNodeModules +``` + +### Testing +```bash +# Run all tests +pnpm test + +# Run specific framework tests +cd frameworks/react-cra && pnpm test +cd frameworks/solid && pnpm test + +# Test coverage in packages +cd packages/cta-engine && pnpm test:coverage +``` + +### Development Workflow +```bash +# 1. Install dependencies +pnpm install + +# 2. Build everything +pnpm build + +# 3. Create a test application (from outside the monorepo) +node [path-to-repo]/cli/create-tsrouter-app/dist/index.js my-app + +# 4. Run in development mode +pnpm dev +``` + +### Publishing (CI Only) +```bash +pnpm cipublish +``` + +## Architecture + +This is a monorepo for TanStack application builders, using pnpm workspaces and Nx for orchestration. + +### Key Concepts + +- **CTA (Create TanStack Application)**: The core system for creating TanStack applications +- **Frameworks**: Technology-specific implementations (React, Solid, etc.) +- **Add-ons**: Plugins that extend application capabilities (e.g., tanstack-query, clerk, sentry) +- **Starters**: Pre-configured application templates with modern defaults +- **Code Router vs File Router**: Two routing modes - code-based or file-based routing + +### Project Structure + +- **cli/**: CLI applications (create-tsrouter-app, create-tanstack, create-start-app) + - Each CLI delegates to @tanstack/cta-cli for core functionality + +- **packages/**: Core packages + - **cta-cli**: Command line interface logic + - **cta-engine**: Core engine for app generation and modification + - **cta-ui**: Web UI for interactive app creation + - **cta-ui-base**: Shared UI components + +- **frameworks/**: Framework implementations + - **react-cra**: React framework support + - **solid**: Solid framework support + - Each contains add-ons, toolchains, and project templates + +### Template System + +Uses EJS templating with these key variables: +- `typescript`, `tailwind`: Boolean flags +- `js`, `jsx`: File extensions based on TypeScript setting +- `fileRouter`, `codeRouter`: Routing mode flags +- `addOnEnabled`: Object of enabled add-ons +- `packageManager`: npm, yarn, or pnpm + +### Add-on System + +Add-ons modify the generated application by: +1. Adding dependencies via package.json +2. Copying asset files +3. Providing demo routes +4. Integrating with the build system + +Custom add-ons can be created as JSON files and loaded via URL. + +### Testing Add-ons and Starters + +```bash +# Serve add-on/starter locally +npx static-server + +# Test add-on +node [repo]/cli/create-tsrouter-app/dist/index.js app-test --add-ons http://localhost:9080/add-on.json + +# Test starter +node [repo]/cli/create-tsrouter-app/dist/index.js app-test --starter http://localhost:9080/starter.json +``` + +### UI Development + +The UI runs as both a web server and React app: + +```bash +# 1. Start API server (from empty directory) +CTA_DISABLE_UI=true node ../create-tsrouter-app/cli/create-tsrouter-app/dist/index.js --ui + +# 2. Start React dev server +cd packages/cta-ui && pnpm dev:ui + +# 3. Run monorepo in watch mode +pnpm dev +``` + +## Key Implementation Details + +- All workspace dependencies use `workspace:*` protocol +- EJS templates use special naming: `_dot_` prefix becomes `.` in output +- Add-ons can provide demo routes that integrate with the router +- The engine uses memfs for virtual file system operations during generation +- Special steps system handles post-generation tasks (e.g., shadcn setup) \ No newline at end of file diff --git a/CUSTOM_PROPERTIES_DESIGN.md b/CUSTOM_PROPERTIES_DESIGN.md new file mode 100644 index 00000000..eaf8c87a --- /dev/null +++ b/CUSTOM_PROPERTIES_DESIGN.md @@ -0,0 +1,467 @@ +# Custom Properties Design for CTA Framework + +## Overview + +This document outlines the design for replacing the hardcoded routes and integrations system with a generic `customProperties` approach using Zod schemas. This will allow framework definitions to declare their own custom properties that can be added to add-ons, providing a more flexible and extensible system. + +## Current System Analysis + +### Current Implementation +- **Routes**: Hardcoded array of route objects in `AddOnBaseSchema` +- **Integrations**: Hardcoded array of integration objects in `AddOnInfoSchema` +- **Template Processing**: Direct access to `routes` and `integrations` arrays in templates +- **Type Safety**: Fixed TypeScript types for routes and integrations + +### Limitations +1. Framework-specific concepts (routes/integrations) are baked into the core engine +2. No flexibility for frameworks to define their own custom properties +3. All frameworks must use the same structure for routes and integrations +4. Cannot easily add new property types without modifying core engine + +## Proposed Design + +### Core Concept + +Replace hardcoded `routes` and `integrations` with a generic `customProperties` system where: +1. Frameworks define their own custom property schemas using Zod +2. Add-ons provide values matching these schemas +3. Templates access properties through a unified interface +4. Type safety is maintained through Zod inference + +### Framework Definition Structure + +```typescript +// Example: React framework definition +export function createFrameworkDefinition(): FrameworkDefinition { + return { + id: 'react-cra', + name: 'React', + customProperties: { + routes: z.array( + z.object({ + url: z.string().optional(), + name: z.string().optional(), + path: z.string(), + jsName: z.string(), + }) + ).optional(), + integrations: z.array( + z.object({ + type: z.enum(['provider', 'root-provider', 'layout', 'header-user']), + path: z.string(), + jsName: z.string(), + }) + ).optional(), + }, + // ... rest of definition + } +} +``` + +### Type Updates + +```typescript +// packages/cta-engine/src/types.ts + +import type { ZodTypeAny } from 'zod' + +// Update FrameworkDefinition to include customProperties +export type FrameworkDefinition = { + id: string + name: string + description: string + version: string + + // New field for custom property schemas + customProperties?: Record + + base: Record + addOns: Array + basePackageJSON: Record + optionalPackages: Record + + supportedModes: Record< + string, + { + displayName: string + description: string + forceTypescript: boolean + } + > +} + +// Remove routes from AddOnBaseSchema +export const AddOnBaseSchema = z.object({ + id: z.string(), + name: z.string(), + description: z.string(), + author: z.string().optional(), + version: z.string().optional(), + link: z.string().optional(), + license: z.string().optional(), + warning: z.string().optional(), + type: z.enum(['add-on', 'example', 'starter', 'toolchain']), + command: z + .object({ + command: z.string(), + args: z.array(z.string()).optional(), + }) + .optional(), + // Remove routes - it will be in customProperties + packageAdditions: z + .object({ + dependencies: z.record(z.string(), z.string()).optional(), + devDependencies: z.record(z.string(), z.string()).optional(), + scripts: z.record(z.string(), z.string()).optional(), + }) + .optional(), + shadcnComponents: z.array(z.string()).optional(), + dependsOn: z.array(z.string()).optional(), + smallLogo: z.string().optional(), + logo: z.string().optional(), + addOnSpecialSteps: z.array(z.string()).optional(), + createSpecialSteps: z.array(z.string()).optional(), +}) + +// Update AddOnInfoSchema to remove integrations +export const AddOnInfoSchema = AddOnBaseSchema.extend({ + modes: z.array(z.string()), + phase: z.enum(['setup', 'add-on']), + readme: z.string().optional(), + // Add customProperties field + customProperties: z.record(z.string(), z.unknown()).optional(), +}) + +// Remove the separate Integration schema as it will be framework-specific +// export const IntegrationSchema = z.object({...}) - REMOVE +``` + +### Add-on Loading and Validation + +```typescript +// packages/cta-engine/src/add-on-loader.ts + +export async function loadAddOn( + addOnPath: string, + framework: Framework +): Promise { + const info = await loadAddOnInfo(addOnPath) + + // Validate custom properties against framework schema + if (framework.customProperties && info.customProperties) { + const validatedProperties: Record = {} + + for (const [key, schema] of Object.entries(framework.customProperties)) { + if (key in info.customProperties) { + try { + validatedProperties[key] = schema.parse(info.customProperties[key]) + } catch (error) { + throw new Error( + `Invalid custom property "${key}" in add-on "${info.id}": ${error.message}` + ) + } + } + } + + info.customProperties = validatedProperties + } + + return { + ...info, + getFiles, + getFileContents, + getDeletedFiles, + } +} +``` + +### Template Processing Updates + +```typescript +// packages/cta-engine/src/template-file.ts + +export function createTemplateFile(environment: Environment, options: Options) { + // Collect all custom properties from add-ons + const customProperties: Record> = {} + + // Initialize arrays for all framework custom properties + if (options.framework.customProperties) { + for (const key of Object.keys(options.framework.customProperties)) { + customProperties[key] = [] + } + } + + // Collect custom properties from each add-on + for (const addOn of options.chosenAddOns) { + if (addOn.customProperties) { + for (const [key, values] of Object.entries(addOn.customProperties)) { + if (customProperties[key] && Array.isArray(values)) { + customProperties[key].push(...values) + } else if (customProperties[key] && !Array.isArray(values)) { + customProperties[key].push(values) + } + } + } + } + + return async function templateFile(file: string, content: string) { + const templateValues = { + // ... existing values + packageManager: options.packageManager, + projectName: options.projectName, + typescript: options.typescript, + tailwind: options.tailwind, + js: options.typescript ? 'ts' : 'js', + jsx: options.typescript ? 'tsx' : 'jsx', + fileRouter: options.mode === 'file-router', + codeRouter: options.mode === 'code-router', + addOnEnabled, + addOns: options.chosenAddOns, + + // Add custom properties dynamically + ...customProperties, + + // Helper functions + getPackageManagerAddScript, + getPackageManagerRunScript, + relativePath: (path: string) => relativePath(file, path), + ignoreFile: () => { + throw new IgnoreFileError() + }, + } + + // ... rest of template processing + } +} +``` + +## Implementation Plan + +### 1. Core Engine Updates +- [ ] Add `customProperties` to `FrameworkDefinition` type +- [ ] Remove `routes` from `AddOnBaseSchema` +- [ ] Remove `integrations` and `IntegrationSchema` from types +- [ ] Add `customProperties` field to `AddOnInfoSchema` +- [ ] Update add-on loader to validate custom properties +- [ ] Update template processor to expose custom properties + +### 2. Framework Updates +- [ ] Update React framework to define routes/integrations schemas +- [ ] Update Solid framework similarly +- [ ] Update all framework templates to use customProperties + +### 3. Add-on Updates +- [ ] Update all add-on info.json files to use customProperties +- [ ] Update add-on documentation +- [ ] Remove routes/integrations from root level + +### 4. Template Updates +- [ ] Update all EJS templates to access properties from customProperties +- [ ] Update navigation generation to use customProperties.routes +- [ ] Update integration rendering to use customProperties.integrations + +## Benefits + +1. **Extensibility**: Frameworks can define any custom properties they need +2. **Type Safety**: Zod schemas provide runtime validation and TypeScript types +3. **Flexibility**: Different frameworks can have different property structures +4. **Future-Proof**: New property types can be added without core engine changes +5. **Framework-Specific**: Each framework can have its own domain-specific concepts +6. **Clean Architecture**: No framework-specific concepts in core engine + +## Example: Framework-Specific Properties + +### React Framework +```typescript +customProperties: { + routes: z.array( + z.object({ + url: z.string().optional(), + name: z.string().optional(), + path: z.string(), + jsName: z.string(), + }) + ).optional(), + integrations: z.array( + z.object({ + type: z.enum(['provider', 'root-provider', 'layout', 'header-user']), + path: z.string(), + jsName: z.string(), + }) + ).optional(), + contextProviders: z.array( + z.object({ + name: z.string(), + path: z.string(), + wrapperComponent: z.string(), + }) + ).optional(), + hooks: z.array( + z.object({ + name: z.string(), + path: z.string(), + category: z.enum(['state', 'effect', 'context', 'custom']), + }) + ).optional(), +} +``` + +### Solid Framework +```typescript +customProperties: { + routes: z.array( + z.object({ + url: z.string().optional(), + name: z.string().optional(), + path: z.string(), + jsName: z.string(), + }) + ).optional(), + integrations: z.array( + z.object({ + type: z.enum(['provider', 'root-provider', 'layout', 'header-user']), + path: z.string(), + jsName: z.string(), + }) + ).optional(), + signals: z.array( + z.object({ + name: z.string(), + path: z.string(), + global: z.boolean(), + }) + ).optional(), + stores: z.array( + z.object({ + name: z.string(), + path: z.string(), + type: z.enum(['store', 'mutable']), + }) + ).optional(), +} +``` + +### Vue Framework (hypothetical) +```typescript +customProperties: { + routes: z.array( + z.object({ + url: z.string().optional(), + name: z.string().optional(), + path: z.string(), + jsName: z.string(), + }) + ).optional(), + components: z.array( + z.object({ + name: z.string(), + path: z.string(), + global: z.boolean(), + }) + ).optional(), + composables: z.array( + z.object({ + name: z.string(), + path: z.string(), + autoImport: z.boolean(), + }) + ).optional(), + plugins: z.array( + z.object({ + name: z.string(), + path: z.string(), + options: z.record(z.string(), z.unknown()).optional(), + }) + ).optional(), +} +``` + +## Add-on info.json Structure + +### Before +```json +{ + "id": "tanstack-query", + "name": "Query", + "description": "Integrate TanStack Query into your application.", + "routes": [ + { + "url": "/demo/tanstack-query", + "name": "TanStack Query", + "path": "src/routes/demo.tanstack-query.tsx", + "jsName": "TanStackQueryDemo" + } + ], + "integrations": [ + { + "type": "root-provider", + "path": "src/integrations/tanstack-query/root-provider.tsx", + "jsName": "TanStackQueryProvider" + } + ] +} +``` + +### After +```json +{ + "id": "tanstack-query", + "name": "Query", + "description": "Integrate TanStack Query into your application.", + "customProperties": { + "routes": [ + { + "url": "/demo/tanstack-query", + "name": "TanStack Query", + "path": "src/routes/demo.tanstack-query.tsx", + "jsName": "TanStackQueryDemo" + } + ], + "integrations": [ + { + "type": "root-provider", + "path": "src/integrations/tanstack-query/root-provider.tsx", + "jsName": "TanStackQueryProvider" + } + ] + } +} +``` + +## Template Access Pattern + +### Before +```ejs +<% for(const route of routes) { %> + <%= route.name %> +<% } %> + +<% for(const integration of integrations.filter(i => i.type === 'provider')) { %> + <<%= integration.jsName %>> +<% } %> +``` + +### After +```ejs +<% for(const route of routes) { %> + <%= route.name %> +<% } %> + +<% for(const integration of integrations.filter(i => i.type === 'provider')) { %> + <<%= integration.jsName %>> +<% } %> +``` + +The template access remains the same because `customProperties` are spread into the template values, making `routes` and `integrations` available at the top level. + +## Testing Strategy + +1. **Unit Tests**: Test custom property validation and merging +2. **Integration Tests**: Test full app generation with custom properties +3. **Framework Tests**: Test each framework's custom properties +4. **Template Tests**: Verify templates render correctly with new system +5. **Add-on Tests**: Test all add-ons work with new structure + +## Conclusion + +The custom properties system provides a flexible, type-safe way for frameworks to extend the add-on system with their own domain-specific concepts. By removing hardcoded framework-specific properties from the core engine, we create a truly extensible system that can adapt to any framework's needs. \ No newline at end of file diff --git a/examples/custom-cli/create-qwik-app/add-ons/feather-icons/info.json b/examples/custom-cli/create-qwik-app/add-ons/feather-icons/info.json index 471f0fcf..e97ef786 100644 --- a/examples/custom-cli/create-qwik-app/add-ons/feather-icons/info.json +++ b/examples/custom-cli/create-qwik-app/add-ons/feather-icons/info.json @@ -7,14 +7,12 @@ ], "type": "add-on", "link": "https://github.com/egmaleta/qwik-feather-icons", - "customProperties": { - "routes": [ - { - "url": "/demo-feather", - "name": "Feather Icons", - "path": "src/routes/demo-feather/index.tsx", - "jsName": "FeatherIconsDemo" - } - ] - } + "routes": [ + { + "url": "/demo-feather", + "name": "Feather Icons", + "path": "src/routes/demo-feather/index.tsx", + "jsName": "FeatherIconsDemo" + } + ] } diff --git a/frameworks/react-cra/add-ons/clerk/info.json b/frameworks/react-cra/add-ons/clerk/info.json index 2fe5a353..f07ace12 100644 --- a/frameworks/react-cra/add-ons/clerk/info.json +++ b/frameworks/react-cra/add-ons/clerk/info.json @@ -7,26 +7,24 @@ ], "type": "add-on", "link": "https://clerk.com", - "customProperties": { - "routes": [ - { - "url": "/demo/clerk", - "name": "Clerk", - "path": "src/routes/demo.clerk.tsx", - "jsName": "ClerkDemo" - } - ], - "integrations": [ - { - "type": "header-user", - "jsName": "ClerkHeader", - "path": "src/integrations/clerk/header-user.tsx" - }, - { - "type": "provider", - "jsName": "ClerkProvider", - "path": "src/integrations/clerk/provider.tsx" - } - ] - } + "routes": [ + { + "url": "/demo/clerk", + "name": "Clerk", + "path": "src/routes/demo.clerk.tsx", + "jsName": "ClerkDemo" + } + ], + "integrations": [ + { + "type": "header-user", + "jsName": "ClerkHeader", + "path": "src/integrations/clerk/header-user.tsx" + }, + { + "type": "provider", + "jsName": "ClerkProvider", + "path": "src/integrations/clerk/provider.tsx" + } + ] } diff --git a/frameworks/react-cra/add-ons/convex/info.json b/frameworks/react-cra/add-ons/convex/info.json index 1af89d05..f06825c8 100644 --- a/frameworks/react-cra/add-ons/convex/info.json +++ b/frameworks/react-cra/add-ons/convex/info.json @@ -7,21 +7,19 @@ "modes": [ "file-router" ], - "customProperties": { - "routes": [ - { - "url": "/demo/convex", - "name": "Convex", - "path": "src/routes/demo.convex.tsx", - "jsName": "ConvexDemo" - } - ], - "integrations": [ - { - "type": "provider", - "path": "src/integrations/convex/provider.tsx", - "jsName": "ConvexProvider" - } - ] - } + "routes": [ + { + "url": "/demo/convex", + "name": "Convex", + "path": "src/routes/demo.convex.tsx", + "jsName": "ConvexDemo" + } + ], + "integrations": [ + { + "type": "provider", + "path": "src/integrations/convex/provider.tsx", + "jsName": "ConvexProvider" + } + ] } diff --git a/frameworks/react-cra/add-ons/form/info.json b/frameworks/react-cra/add-ons/form/info.json index 9748f693..d2b9fde1 100644 --- a/frameworks/react-cra/add-ons/form/info.json +++ b/frameworks/react-cra/add-ons/form/info.json @@ -17,20 +17,18 @@ "switch", "label" ], - "customProperties": { - "routes": [ - { - "url": "/demo/form/simple", - "name": "Simple Form", - "path": "src/routes/demo.form.simple.tsx", - "jsName": "FormSimpleDemo" - }, - { - "url": "/demo/form/address", - "name": "Address Form", - "path": "src/routes/demo.form.address.tsx", - "jsName": "FormAddressDemo" - } - ] - } + "routes": [ + { + "url": "/demo/form/simple", + "name": "Simple Form", + "path": "src/routes/demo.form.simple.tsx", + "jsName": "FormSimpleDemo" + }, + { + "url": "/demo/form/address", + "name": "Address Form", + "path": "src/routes/demo.form.address.tsx", + "jsName": "FormAddressDemo" + } + ] } diff --git a/frameworks/react-cra/add-ons/neon/info.json b/frameworks/react-cra/add-ons/neon/info.json index b6ea638d..f9cdc2e0 100644 --- a/frameworks/react-cra/add-ons/neon/info.json +++ b/frameworks/react-cra/add-ons/neon/info.json @@ -10,14 +10,12 @@ "dependsOn": [ "start" ], - "customProperties": { - "routes": [ - { - "url": "/demo/neon", - "name": "Neon", - "path": "src/routes/demo.neon.tsx", - "jsName": "NeonDemo" - } - ] - } + "routes": [ + { + "url": "/demo/neon", + "name": "Neon", + "path": "src/routes/demo.neon.tsx", + "jsName": "NeonDemo" + } + ] } diff --git a/frameworks/react-cra/add-ons/sentry/info.json b/frameworks/react-cra/add-ons/sentry/info.json index 22c98423..f393e54a 100644 --- a/frameworks/react-cra/add-ons/sentry/info.json +++ b/frameworks/react-cra/add-ons/sentry/info.json @@ -10,14 +10,12 @@ "dependsOn": [ "start" ], - "customProperties": { - "routes": [ - { - "url": "/demo/sentry/testing", - "name": "Sentry", - "path": "src/routes/demo.sentry.testing.tsx", - "jsName": "SentryDemo" - } - ] - } + "routes": [ + { + "url": "/demo/sentry/testing", + "name": "Sentry", + "path": "src/routes/demo.sentry.testing.tsx", + "jsName": "SentryDemo" + } + ] } diff --git a/frameworks/react-cra/add-ons/start/info.json b/frameworks/react-cra/add-ons/start/info.json index a8cd9f13..a6771c71 100644 --- a/frameworks/react-cra/add-ons/start/info.json +++ b/frameworks/react-cra/add-ons/start/info.json @@ -16,20 +16,18 @@ "addOnSpecialSteps": [ "rimraf-node-modules" ], - "customProperties": { - "routes": [ - { - "url": "/demo/start/server-funcs", - "name": "Start - Server Functions", - "path": "src/routes/demo.start.server-funcs.tsx", - "jsName": "StartServerFuncsDemo" - }, - { - "url": "/demo/start/api-request", - "name": "Start - API Request", - "path": "src/routes/demo.start.api-request.tsx", - "jsName": "StartApiRequestDemo" - } - ] - } + "routes": [ + { + "url": "/demo/start/server-funcs", + "name": "Start - Server Functions", + "path": "src/routes/demo.start.server-funcs.tsx", + "jsName": "StartServerFuncsDemo" + }, + { + "url": "/demo/start/api-request", + "name": "Start - API Request", + "path": "src/routes/demo.start.api-request.tsx", + "jsName": "StartApiRequestDemo" + } + ] } diff --git a/frameworks/react-cra/add-ons/store/info.json b/frameworks/react-cra/add-ons/store/info.json index 5c231a2f..933622f8 100644 --- a/frameworks/react-cra/add-ons/store/info.json +++ b/frameworks/react-cra/add-ons/store/info.json @@ -8,14 +8,12 @@ "file-router", "code-router" ], - "customProperties": { - "routes": [ - { - "url": "/demo/store", - "name": "Store", - "path": "src/routes/demo.store.tsx", - "jsName": "StoreDemo" - } - ] - } + "routes": [ + { + "url": "/demo/store", + "name": "Store", + "path": "src/routes/demo.store.tsx", + "jsName": "StoreDemo" + } + ] } diff --git a/frameworks/react-cra/add-ons/table/info.json b/frameworks/react-cra/add-ons/table/info.json index 59d9fac8..fe90bf33 100644 --- a/frameworks/react-cra/add-ons/table/info.json +++ b/frameworks/react-cra/add-ons/table/info.json @@ -8,14 +8,12 @@ ], "link": "https://tanstack.com/table/latest", "type": "add-on", - "customProperties": { - "routes": [ - { - "url": "/demo/table", - "name": "TanStack Table", - "path": "src/routes/demo.table.tsx", - "jsName": "TableDemo" - } - ] - } + "routes": [ + { + "url": "/demo/table", + "name": "TanStack Table", + "path": "src/routes/demo.table.tsx", + "jsName": "TableDemo" + } + ] } diff --git a/frameworks/react-cra/add-ons/tanstack-query/info.json b/frameworks/react-cra/add-ons/tanstack-query/info.json index 1fdbd1a4..0f3d9d9e 100644 --- a/frameworks/react-cra/add-ons/tanstack-query/info.json +++ b/frameworks/react-cra/add-ons/tanstack-query/info.json @@ -5,26 +5,24 @@ "modes": ["file-router", "code-router"], "type": "add-on", "link": "https://tanstack.com/query/latest", - "customProperties": { - "routes": [ - { - "url": "/demo/tanstack-query", - "name": "TanStack Query", - "path": "src/routes/demo.tanstack-query.tsx", - "jsName": "TanStackQueryDemo" - } - ], - "integrations": [ - { - "type": "root-provider", - "path": "src/integrations/tanstack-query/root-provider.tsx", - "jsName": "TanStackQueryProvider" - }, - { - "type": "layout", - "path": "src/integrations/tanstack-query/layout.tsx", - "jsName": "TanStackQueryLayout" - } - ] - } + "routes": [ + { + "url": "/demo/tanstack-query", + "name": "TanStack Query", + "path": "src/routes/demo.tanstack-query.tsx", + "jsName": "TanStackQueryDemo" + } + ], + "integrations": [ + { + "type": "root-provider", + "path": "src/integrations/tanstack-query/root-provider.tsx", + "jsName": "TanStackQueryProvider" + }, + { + "type": "layout", + "path": "src/integrations/tanstack-query/layout.tsx", + "jsName": "TanStackQueryLayout" + } + ] } diff --git a/frameworks/solid/add-ons/form/info.json b/frameworks/solid/add-ons/form/info.json index 25969429..29d757e1 100644 --- a/frameworks/solid/add-ons/form/info.json +++ b/frameworks/solid/add-ons/form/info.json @@ -8,14 +8,13 @@ "code-router" ], "type": "add-on", - "customProperties": { - "routes": [ - { - "url": "/demo/form", - "name": "Form", - "path": "src/routes/demo.form.tsx", - "jsName": "FormDemo" - } - ] - } + "routes": [ + { + "url": "/demo/form", + "name": "Form", + "path": "src/routes/demo.form.tsx", + "jsName": "FormDemo" + } + ], + "customProperties": {} } diff --git a/frameworks/solid/add-ons/sentry/info.json b/frameworks/solid/add-ons/sentry/info.json index 0656dc34..9171ddee 100644 --- a/frameworks/solid/add-ons/sentry/info.json +++ b/frameworks/solid/add-ons/sentry/info.json @@ -7,14 +7,13 @@ "file-router" ], "type": "add-on", - "customProperties": { - "routes": [ - { - "url": "/demo/sentry/bad-event-handler", - "name": "Sentry", - "path": "src/routes/demo.sentry.bad-event-handler.tsx", - "jsName": "SentryBadEventHandlerDemo" - } - ] - } + "routes": [ + { + "url": "/demo/sentry/bad-event-handler", + "name": "Sentry", + "path": "src/routes/demo.sentry.bad-event-handler.tsx", + "jsName": "SentryBadEventHandlerDemo" + } + ], + "customProperties": {} } diff --git a/frameworks/solid/add-ons/start/info.json b/frameworks/solid/add-ons/start/info.json index 0f346c91..b860dbdb 100644 --- a/frameworks/solid/add-ons/start/info.json +++ b/frameworks/solid/add-ons/start/info.json @@ -8,14 +8,13 @@ ], "type": "add-on", "warning": "TanStack Start is not yet at 1.0 and may change significantly or not be compatible with other add-ons.", - "customProperties": { - "routes": [ - { - "url": "/demo/start/server-funcs", - "name": "Start - Server Functions", - "path": "src/routes/demo.start.server-funcs.tsx", - "jsName": "StartServerFuncsDemo" - } - ] - } + "routes": [ + { + "url": "/demo/start/server-funcs", + "name": "Start - Server Functions", + "path": "src/routes/demo.start.server-funcs.tsx", + "jsName": "StartServerFuncsDemo" + } + ], + "customProperties": {} } diff --git a/frameworks/solid/add-ons/store/info.json b/frameworks/solid/add-ons/store/info.json index a29997f2..1f590b45 100644 --- a/frameworks/solid/add-ons/store/info.json +++ b/frameworks/solid/add-ons/store/info.json @@ -8,14 +8,13 @@ ], "type": "add-on", "link": "https://tanstack.com/store/latest", - "customProperties": { - "routes": [ - { - "url": "/demo/store", - "name": "Store", - "path": "src/routes/demo.store.tsx", - "jsName": "StoreDemo" - } - ] - } + "routes": [ + { + "url": "/demo/store", + "name": "Store", + "path": "src/routes/demo.store.tsx", + "jsName": "StoreDemo" + } + ], + "customProperties": {} } diff --git a/frameworks/solid/add-ons/tanstack-query/info.json b/frameworks/solid/add-ons/tanstack-query/info.json index 798ccbb3..aba2e208 100644 --- a/frameworks/solid/add-ons/tanstack-query/info.json +++ b/frameworks/solid/add-ons/tanstack-query/info.json @@ -7,26 +7,25 @@ ], "type": "add-on", "link": "https://tanstack.com/query/latest", - "customProperties": { - "routes": [ - { - "url": "/demo/tanstack-query", - "name": "TanStack Query", - "path": "src/routes/demo.tanstack-query.tsx", - "jsName": "TanStackQueryDemo" - } - ], - "integrations": [ - { - "type": "provider", - "path": "src/integrations/tanstack-query/provider.tsx", - "jsName": "TanStackQueryProvider" - }, - { - "type": "header-user", - "path": "src/integrations/tanstack-query/header-user.tsx", - "jsName": "TanStackQueryHeaderUser" - } - ] - } + "routes": [ + { + "url": "/demo/tanstack-query", + "name": "TanStack Query", + "path": "src/routes/demo.tanstack-query.tsx", + "jsName": "TanStackQueryDemo" + } + ], + "integrations": [ + { + "type": "provider", + "path": "src/integrations/tanstack-query/provider.tsx", + "jsName": "TanStackQueryProvider" + }, + { + "type": "header-user", + "path": "src/integrations/tanstack-query/header-user.tsx", + "jsName": "TanStackQueryHeaderUser" + } + ], + "customProperties": {} } diff --git a/frameworks/solid/examples/tanchat/info.json b/frameworks/solid/examples/tanchat/info.json index b1a626b3..61b09108 100644 --- a/frameworks/solid/examples/tanchat/info.json +++ b/frameworks/solid/examples/tanchat/info.json @@ -8,7 +8,9 @@ "routes": [ { "url": "/example/chat", - "name": "Chat" + "name": "Chat", + "path": "src/routes/example.chat.tsx", + "jsName": "ChatDemo" } ], "dependsOn": ["solid-ui", "store"] diff --git a/packages/cta-engine/ROUTES_AND_INTEGRATIONS.md b/packages/cta-engine/ROUTES_AND_INTEGRATIONS.md new file mode 100644 index 00000000..3026f810 --- /dev/null +++ b/packages/cta-engine/ROUTES_AND_INTEGRATIONS.md @@ -0,0 +1,448 @@ +# Routes and Integrations in CTA Engine + +This document explains how the routes and integrations systems work in the Create TanStack Application (CTA) engine. These two concepts are fundamental to how add-ons extend and enhance generated applications. + +## Table of Contents + +1. [Overview](#overview) +2. [Routes System](#routes-system) +3. [Integrations System](#integrations-system) +4. [Implementation Details](#implementation-details) +5. [Add-on Examples](#add-on-examples) +6. [Creating Custom Add-ons](#creating-custom-add-ons) + +## Overview + +The CTA engine uses two primary mechanisms for add-ons to extend applications: + +- **Routes**: Demo pages and API endpoints that showcase add-on functionality +- **Integrations**: Components and providers that integrate into the application's structure + +Both systems work together to provide a seamless way for add-ons to enhance applications without manual configuration. + +## Routes System + +### What are Routes? + +Routes are demo pages or API endpoints that add-ons provide to showcase their functionality. They are automatically registered with the TanStack Router when an add-on is selected during app creation. + +### Route Structure + +Routes are defined in an add-on's `info.json` file: + +```json +{ + "routes": [ + { + "url": "/demo/tanstack-query", + "name": "TanStack Query", + "path": "src/routes/demo.tanstack-query.tsx", + "jsName": "TanStackQueryDemo" + } + ] +} +``` + +#### Route Properties + +- **url** (optional): The URL path where the route will be accessible +- **name** (optional): Display name for navigation links +- **path**: File path relative to the add-on's assets directory +- **jsName**: JavaScript/TypeScript identifier used when importing the route + +### How Routes are Processed + +1. **Collection Phase**: During app generation, the template engine collects all routes from selected add-ons: + +```typescript +// From template-file.ts +const routes: Array['routes'][number]> = [] +for (const addOn of options.chosenAddOns) { + if (addOn.routes) { + routes.push(...addOn.routes) + } +} +``` + +2. **Template Processing**: Routes are made available to EJS templates via the `routes` variable + +3. **Router Integration**: Routes are integrated differently based on the routing mode: + +#### Code Router Mode + +In code router mode, routes are imported and registered directly in `main.tsx`: + +```typescript +// Import route components +<% for(const route of routes) { %> +import <%= route.jsName %> from "<%= relativePath(route.path) %>"; +<% } %> + +// Register routes with the router +const routeTree = rootRoute.addChildren([ + indexRoute<%= routes.map(route => `, ${route.jsName}(rootRoute)`).join('') %> +]); +``` + +#### File Router Mode + +In file router mode, route files are copied to the appropriate location in the routes directory, and the TanStack Router file-based routing system automatically discovers them. + +### Route File Format + +Route files use the TanStack Router format and are automatically templatized to work in both routing modes: + +```typescript +// Original route file +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/demo/my-feature')({ + component: MyFeatureDemo, +}) + +// After templatization (EJS) +import { <% if (fileRouter) { %>createFileRoute<% } else { %>createRoute<% } %> } from '@tanstack/react-router' + +<% if (codeRouter) { %> +import type { RootRoute } from '@tanstack/react-router' + +export default (parentRoute: RootRoute) => createRoute({ + path: '/demo/my-feature', + component: MyFeatureDemo, + getParentRoute: () => parentRoute, +}) +<% } else { %> +export const Route = createFileRoute('/demo/my-feature')({ + component: MyFeatureDemo, +}) +<% } %> +``` + +## Integrations System + +### What are Integrations? + +Integrations are components and providers that need to be injected into specific locations within the application structure. They allow add-ons to wrap the application with providers, add layout components, or inject UI elements into the header. + +### Integration Types + +The CTA engine supports four types of integrations: + +#### 1. Provider Integration + +Wraps the application or router with a context provider: + +```json +{ + "type": "provider", + "jsName": "ClerkProvider", + "path": "src/integrations/clerk/provider.tsx" +} +``` + +Providers are rendered in the root layout, wrapping the router outlet: + +```tsx +<% for(const integration of integrations.filter(i => i.type === 'provider')) { %> + <<%= integration.jsName %>> +<% } %> + +<% for(const integration of integrations.filter(i => i.type === 'provider').reverse()) { %> + > +<% } %> +``` + +#### 2. Root Provider Integration + +Special providers that wrap the entire application and provide context to the router: + +```json +{ + "type": "root-provider", + "path": "src/integrations/tanstack-query/root-provider.tsx", + "jsName": "TanStackQueryProvider" +} +``` + +Root providers must export: +- `Provider`: React component that wraps the app +- `getContext()`: Function that returns context for the router + +```tsx +// Import root providers +<% for(const integration of integrations.filter(i => i.type === 'root-provider')) { %> +import * as <%= integration.jsName %> from "<%= relativePath(integration.path) %>"; +<% } %> + +// Add context to router +const router = createRouter({ + routeTree, + context: { + <% for(const integration of integrations.filter(i => i.type === 'root-provider')) { %> + ...<%= integration.jsName %>.getContext(), + <% } %> + }, +}) + +// Wrap the application +<% for(const integration of integrations.filter(i => i.type === 'root-provider')) { %> + <<%= integration.jsName %>.Provider> +<% } %> + +<% for(const integration of integrations.filter(i => i.type === 'root-provider').reverse()) { %> + .Provider> +<% } %> +``` + +#### 3. Layout Integration + +Components that are rendered alongside the router outlet (e.g., devtools): + +```json +{ + "type": "layout", + "path": "src/integrations/tanstack-query/layout.tsx", + "jsName": "TanStackQueryLayout" +} +``` + +Layout components are rendered after the outlet: + +```tsx + +<% for(const integration of integrations.filter(i => i.type === 'layout')) { %> + <<%= integration.jsName %> /> +<% } %> +``` + +#### 4. Header User Integration + +Components that are injected into the application header (e.g., user menus): + +```json +{ + "type": "header-user", + "jsName": "ClerkHeader", + "path": "src/integrations/clerk/header-user.tsx" +} +``` + +Header user components are rendered in the header component: + +```tsx +<% if (integrations.filter(i => i.type === 'header-user').length > 0) { %> +
+ <% for(const integration of integrations.filter(i => i.type === 'header-user')) { %> + <<%= integration.jsName %> /> + <% } %> +
+<% } %> +``` + +## Implementation Details + +### Type Definitions + +The TypeScript types for routes and integrations are defined in `types.ts`: + +```typescript +// Integration type definition +export const IntegrationSchema = z.object({ + type: z.string(), + path: z.string(), + jsName: z.string(), +}) + +// Add-on schema includes both routes and integrations +export const AddOnInfoSchema = AddOnBaseSchema.extend({ + routes: z.array( + z.object({ + url: z.string().optional(), + name: z.string().optional(), + path: z.string(), + jsName: z.string(), + }), + ).optional(), + integrations: z.array(IntegrationSchema).optional(), + // ... other properties +}) +``` + +### Template Processing + +The template engine (`template-file.ts`) makes routes and integrations available to all EJS templates: + +```typescript +// Collect integrations from all add-ons +const integrations: Array['integrations'][number]> = [] +for (const addOn of options.chosenAddOns) { + if (addOn.integrations) { + for (const integration of addOn.integrations) { + integrations.push(integration) + } + } +} + +// Collect routes from all add-ons +const routes: Array['routes'][number]> = [] +for (const addOn of options.chosenAddOns) { + if (addOn.routes) { + routes.push(...addOn.routes) + } +} + +// Make available to templates +const templateValues = { + // ... other values + integrations, + routes, + // ... other values +} +``` + +### Navigation Generation + +The Header component automatically generates navigation links for all routes that have both `url` and `name` properties: + +```tsx +<% for(const addOn of addOns) { + for(const route of (addOn?.routes||[])?.filter(r => r.url && r.name)) { %> +
+ <%= route.name %> +
+<% } } %> +``` + +## Add-on Examples + +### TanStack Query Add-on + +This add-on demonstrates both routes and integrations: + +```json +{ + "name": "Query", + "description": "Integrate TanStack Query into your application.", + "routes": [ + { + "url": "/demo/tanstack-query", + "name": "TanStack Query", + "path": "src/routes/demo.tanstack-query.tsx", + "jsName": "TanStackQueryDemo" + } + ], + "integrations": [ + { + "type": "root-provider", + "path": "src/integrations/tanstack-query/root-provider.tsx", + "jsName": "TanStackQueryProvider" + }, + { + "type": "layout", + "path": "src/integrations/tanstack-query/layout.tsx", + "jsName": "TanStackQueryLayout" + } + ] +} +``` + +### Clerk Authentication Add-on + +This add-on uses provider and header integrations: + +```json +{ + "name": "Clerk", + "description": "Add Clerk authentication to your application.", + "routes": [ + { + "url": "/demo/clerk", + "name": "Clerk", + "path": "src/routes/demo.clerk.tsx", + "jsName": "ClerkDemo" + } + ], + "integrations": [ + { + "type": "header-user", + "jsName": "ClerkHeader", + "path": "src/integrations/clerk/header-user.tsx" + }, + { + "type": "provider", + "jsName": "ClerkProvider", + "path": "src/integrations/clerk/provider.tsx" + } + ] +} +``` + +## Creating Custom Add-ons + +### Best Practices for Routes + +1. **Naming Convention**: Use descriptive names prefixed with `demo.` for demo routes +2. **Path Structure**: Place route files in `assets/src/routes/` +3. **Component Naming**: Use PascalCase for `jsName` (e.g., `MyFeatureDemo`) +4. **URL Pattern**: Use `/demo/feature-name` for demo routes + +### Best Practices for Integrations + +1. **Provider Integrations**: + - Use for wrapping parts of the app with context + - Keep providers focused on a single concern + - Don't add UI elements in providers + +2. **Root Provider Integrations**: + - Use when you need to provide context to the router + - Always export both `Provider` and `getContext()` + - Initialize any required clients or stores + +3. **Layout Integrations**: + - Use for developer tools or persistent UI elements + - Keep layout components lightweight + - Position them appropriately (devtools, tooltips, etc.) + +4. **Header User Integrations**: + - Use for user-specific UI in the header + - Keep components small and focused + - Consider responsive design + +### File Structure + +Organize your add-on files consistently: + +``` +my-addon/ +├── info.json +├── package.json +├── assets/ +│ ├── src/ +│ │ ├── routes/ +│ │ │ └── demo.my-feature.tsx +│ │ └── integrations/ +│ │ └── my-feature/ +│ │ ├── provider.tsx +│ │ └── layout.tsx +│ └── _dot_env.local.append +``` + +### Testing Your Add-on + +1. Create a test project with your add-on +2. Verify routes appear in navigation +3. Check that integrations are properly injected +4. Test in both file-router and code-router modes +5. Ensure TypeScript types work correctly + +### Integration Dependencies + +If your integrations depend on other add-ons, use the `dependsOn` field: + +```json +{ + "dependsOn": ["tanstack-query", "start"] +} +``` + +This ensures required add-ons are included when your add-on is selected. \ No newline at end of file diff --git a/packages/cta-engine/src/custom-add-ons/add-on.ts b/packages/cta-engine/src/custom-add-ons/add-on.ts index b7698e57..86239b41 100644 --- a/packages/cta-engine/src/custom-add-ons/add-on.ts +++ b/packages/cta-engine/src/custom-add-ons/add-on.ts @@ -139,9 +139,7 @@ export async function readOrGenerateAddOnInfo( shadcnComponents: [], framework: options.framework, modes: [options.mode], - customProperties: { - routes: [], - }, + routes: [], warning: '', phase: 'add-on', type: 'add-on', @@ -187,13 +185,10 @@ export async function buildAssetsDirectory( }) if (file.includes('/routes/')) { const { url, code, name, jsName } = templatize(changedFiles[file], file) - if (!info.customProperties) { - info.customProperties = {} - } - if (!info.customProperties.routes) { - info.customProperties.routes = [] + if (!info.routes) { + info.routes = [] } - const routes = info.customProperties.routes as Array + const routes = info.routes as Array if (!routes.find((r: any) => r.url === url)) { routes.push({ url, diff --git a/packages/cta-engine/src/frameworks.ts b/packages/cta-engine/src/frameworks.ts index 2c41dc88..68614066 100644 --- a/packages/cta-engine/src/frameworks.ts +++ b/packages/cta-engine/src/frameworks.ts @@ -116,6 +116,35 @@ export function scanAddOnDirectories( validatedCustomProperties = info.customProperties } + // Also validate routes and integrations that are at root level in info.json + // but defined in framework's customProperties + let validatedRoutes = info.routes + let validatedIntegrations = info.integrations + + if (framework?.customProperties) { + // Validate routes if defined in framework customProperties + if (framework.customProperties.routes && info.routes) { + try { + validatedRoutes = framework.customProperties.routes.parse(info.routes) + } catch (error: any) { + throw new Error( + `Invalid routes in add-on "${dir}": ${error.message}` + ) + } + } + + // Validate integrations if defined in framework customProperties + if (framework.customProperties.integrations && info.integrations) { + try { + validatedIntegrations = framework.customProperties.integrations.parse(info.integrations) + } catch (error: any) { + throw new Error( + `Invalid integrations in add-on "${dir}": ${error.message}` + ) + } + } + } + addOns.push({ ...info, id: dir, @@ -123,6 +152,8 @@ export function scanAddOnDirectories( readme, files, smallLogo, + routes: validatedRoutes, + integrations: validatedIntegrations, customProperties: validatedCustomProperties, getFiles, getFileContents, diff --git a/packages/cta-engine/src/template-file.ts b/packages/cta-engine/src/template-file.ts index 0de8d38e..b813fcda 100644 --- a/packages/cta-engine/src/template-file.ts +++ b/packages/cta-engine/src/template-file.ts @@ -79,6 +79,26 @@ export function createTemplateFile(environment: Environment, options: Options) { } } + // Collect routes and integrations from add-ons (they're at root level in info.json) + // but they're defined in customProperties in the framework schema + if (options.framework.customProperties?.routes) { + customProperties.routes = [] + for (const addOn of options.chosenAddOns) { + if (addOn.routes) { + customProperties.routes.push(...addOn.routes) + } + } + } + + if (options.framework.customProperties?.integrations) { + customProperties.integrations = [] + for (const addOn of options.chosenAddOns) { + if ('integrations' in addOn && addOn.integrations) { + customProperties.integrations.push(...(addOn as any).integrations) + } + } + } + const addOnEnabled = options.chosenAddOns.reduce>( (acc, addOn) => { acc[addOn.id] = true diff --git a/packages/cta-engine/src/types.ts b/packages/cta-engine/src/types.ts index d2ab15a9..d594c5c8 100644 --- a/packages/cta-engine/src/types.ts +++ b/packages/cta-engine/src/types.ts @@ -26,6 +26,16 @@ export const AddOnBaseSchema = z.object({ args: z.array(z.string()).optional(), }) .optional(), + routes: z + .array( + z.object({ + url: z.string().optional(), + name: z.string().optional(), + path: z.string(), + jsName: z.string(), + }), + ) + .optional(), packageAdditions: z .object({ dependencies: z.record(z.string(), z.string()).optional(), @@ -58,6 +68,15 @@ export const AddOnInfoSchema = AddOnBaseSchema.extend({ modes: z.array(z.string()), phase: z.enum(['setup', 'add-on']), readme: z.string().optional(), + integrations: z + .array( + z.object({ + type: z.string(), + path: z.string(), + jsName: z.string(), + }), + ) + .optional(), customProperties: z.record(z.string(), z.unknown()).optional(), })