From fb848f6be22699cb1047ee17dbffa446fbb28d54 Mon Sep 17 00:00:00 2001 From: Eric Luce <37158449+eluce2@users.noreply.github.com> Date: Fri, 22 Aug 2025 21:01:19 -0500 Subject: [PATCH 1/6] dynamic templates --- packages/cli/package.json | 1 + packages/cli/src/cli/add/index.ts | 5 +- .../cli/src/cli/add/registry/getOptions.ts | 52 ++++---- packages/cli/src/cli/add/registry/install.ts | 82 +++++++----- .../add/registry/postInstall/handlebars.ts | 124 ++++++++++++++++++ .../cli/src/cli/add/registry/preflight.ts | 25 ++-- packages/cli/src/helpers/shadcn-cli.ts | 4 +- packages/cli/src/helpers/stealth-init.ts | 26 ++-- packages/cli/src/utils/parseSettings.ts | 5 +- packages/registry/lib/types.ts | 2 + packages/registry/lib/utils.ts | 6 +- packages/registry/lib/validator.ts | 8 +- .../registry/templates/better-auth/_meta.ts | 1 + .../better-auth/{auth.ts => auth.hbs} | 17 ++- pnpm-lock.yaml | 30 +++++ 15 files changed, 288 insertions(+), 100 deletions(-) create mode 100644 packages/cli/src/cli/add/registry/postInstall/handlebars.ts rename packages/registry/templates/better-auth/{auth.ts => auth.hbs} (80%) diff --git a/packages/cli/package.json b/packages/cli/package.json index d2cc3def..55f2358e 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -73,6 +73,7 @@ "fs-extra": "^11.3.0", "glob": "^11.0.1", "gradient-string": "^2.0.2", + "handlebars": "^4.7.8", "jiti": "^1.21.7", "jsonc-parser": "^3.3.1", "open": "^10.1.0", diff --git a/packages/cli/src/cli/add/index.ts b/packages/cli/src/cli/add/index.ts index 1c68a961..24ca85ed 100644 --- a/packages/cli/src/cli/add/index.ts +++ b/packages/cli/src/cli/add/index.ts @@ -23,15 +23,14 @@ export const runAdd = async ( name: string | undefined, options?: { noInstall?: boolean } ) => { - if (name === "tanstack-query") { return await runAddTanstackQueryCommand(); } else if (name !== undefined) { // an arbitrary name was provided, so we'll try to install from the registry return await installFromRegistry(name); } - - ensureProofKitProject({ commandName: "add" }); + + ensureProofKitProject({ commandName: "add" }); const settings = getSettings(); const addType = abortIfCancel( diff --git a/packages/cli/src/cli/add/registry/getOptions.ts b/packages/cli/src/cli/add/registry/getOptions.ts index 66c13e14..c932067b 100644 --- a/packages/cli/src/cli/add/registry/getOptions.ts +++ b/packages/cli/src/cli/add/registry/getOptions.ts @@ -1,9 +1,9 @@ - -import { registryFetch } from "./http.js"; -import fs from "fs-extra"; -import fg from "fast-glob" import path from "path"; +import fg from "fast-glob"; +import fs from "fs-extra"; + import { state } from "~/state.js"; +import { registryFetch } from "./http.js"; export async function getMetaFromRegistry(name: string) { const result = await registryFetch("@get/meta/:name", { @@ -22,32 +22,30 @@ const PROJECT_SHARED_IGNORE = [ "public", "dist", "build", -] +]; export async function getProjectInfo() { const cwd = state.projectDir || process.cwd(); const [configFiles, isSrcDir] = await Promise.all([ - fg.glob( - "**/{next,vite,astro,app}.config.*|gatsby-config.*|composer.json|react-router.config.*", - { - cwd, - deep: 3, - ignore: PROJECT_SHARED_IGNORE, - } - ), - fs.pathExists(path.resolve(cwd, "src")) - ]) - - -const isUsingAppDir = await fs.pathExists( - path.resolve(cwd, `${isSrcDir ? "src/" : ""}app`) -) - + fg.glob( + "**/{next,vite,astro,app}.config.*|gatsby-config.*|composer.json|react-router.config.*", + { + cwd, + deep: 3, + ignore: PROJECT_SHARED_IGNORE, + } + ), + fs.pathExists(path.resolve(cwd, "src")), + ]); + + const isUsingAppDir = await fs.pathExists( + path.resolve(cwd, `${isSrcDir ? "src/" : ""}app`) + ); + + // Next.js. + if (configFiles.find((file) => file.startsWith("next.config."))?.length) { + return isUsingAppDir ? "next-app" : "next-pages"; + } -// Next.js. -if (configFiles.find((file) => file.startsWith("next.config."))?.length) { - return isUsingAppDir ? "next-app" : "next-pages" + return "manual"; } - -return "manual" -} \ No newline at end of file diff --git a/packages/cli/src/cli/add/registry/install.ts b/packages/cli/src/cli/add/registry/install.ts index ae7e7989..8c6340a3 100644 --- a/packages/cli/src/cli/add/registry/install.ts +++ b/packages/cli/src/cli/add/registry/install.ts @@ -1,29 +1,32 @@ import { getOtherProofKitDependencies } from "@proofkit/registry"; -import semver from "semver"; +import { uniq } from "es-toolkit"; import ora from "ora"; +import semver from "semver"; import { getRegistryUrl, shadcnInstall } from "~/helpers/shadcn-cli.js"; import { getVersion } from "~/utils/getProofKitVersion.js"; import { logger } from "~/utils/logger.js"; import { getSettings, mergeSettings } from "~/utils/parseSettings.js"; import { getMetaFromRegistry } from "./getOptions.js"; +import { + buildHandlebarsData, + getFilePath, + randerHandlebarsToFile, +} from "./postInstall/handlebars.js"; import { processPostInstallStep } from "./postInstall/index.js"; import { preflightAddCommand } from "./preflight.js"; -import { uniq } from "es-toolkit"; export async function installFromRegistry(name: string) { - const spinner = ora("Validating template").start(); await preflightAddCommand(); - + try { const meta = await getMetaFromRegistry(name); if (!meta) { spinner.fail(`Template ${name} not found in the ProofKit registry`); return; } - - + if ( meta.minimumProofKitVersion && semver.gt(meta.minimumProofKitVersion, getVersion()) @@ -36,42 +39,51 @@ export async function installFromRegistry(name: string) { } spinner.succeed(); - const otherProofKitDependencies = getOtherProofKitDependencies(meta); + const otherProofKitDependencies = getOtherProofKitDependencies(meta); - const previouslyInstalledTemplates = getSettings().registryTemplates; + const previouslyInstalledTemplates = getSettings().registryTemplates; - // if dynamic, figure out what fields to pass, then construct the URL to send to shadcn. Otherwise, just send the URL to shadcn - let url = `${getRegistryUrl()}/r/${name}`; - if (meta.type === "dynamic") { - throw new Error("Dynamic templates are not yet supported"); - } else if (meta.type === "static") { - // just send the URL to shadcn - } else { - throw new Error("Unknown template type"); - } + // if dynamic, figure out what fields to pass, then construct the URL to send to shadcn. Otherwise, just send the URL to shadcn + let url = `${getRegistryUrl()}/r/${name}`; + if (meta.type === "dynamic") { + throw new Error("Dynamic templates are not yet supported"); + } else if (meta.type === "static") { + // just send the URL to shadcn + } else { + throw new Error("Unknown template type"); + } + + // run shadcn command + await shadcnInstall([url], meta.title); - // run shadcn command - await shadcnInstall([url], meta.title); + const handlebarsFiles = meta.files.filter((file) => file.handlebars); - // if post-install steps, process those - if (meta.postInstall) { - for (const step of meta.postInstall) { - if (step._from && previouslyInstalledTemplates.includes(step._from)) { - // don't re-run post-install steps for templates that have already been installed - continue; + if (handlebarsFiles.length > 0) { + const templateData = buildHandlebarsData(); + for (const file of handlebarsFiles) { + await randerHandlebarsToFile(file, templateData); + } + } + + // if post-install steps, process those + if (meta.postInstall) { + for (const step of meta.postInstall) { + if (step._from && previouslyInstalledTemplates.includes(step._from)) { + // don't re-run post-install steps for templates that have already been installed + continue; + } + await processPostInstallStep(step); } - await processPostInstallStep(step); } - } - // update the settings - mergeSettings({ - registryTemplates: uniq([ - ...previouslyInstalledTemplates, - name, - ...otherProofKitDependencies, - ]), - }); + // update the settings + mergeSettings({ + registryTemplates: uniq([ + ...previouslyInstalledTemplates, + name, + ...otherProofKitDependencies, + ]), + }); } catch (error) { spinner.fail("Failed to fetch template metadata."); logger.error(error); diff --git a/packages/cli/src/cli/add/registry/postInstall/handlebars.ts b/packages/cli/src/cli/add/registry/postInstall/handlebars.ts new file mode 100644 index 00000000..d5788ec7 --- /dev/null +++ b/packages/cli/src/cli/add/registry/postInstall/handlebars.ts @@ -0,0 +1,124 @@ +import path from "path"; +import { TemplateFile } from "@proofkit/registry"; +import fs from "fs-extra"; +import handlebars from "handlebars"; + +import { getShadcnConfig } from "~/helpers/shadcn-cli.js"; +import { state } from "~/state.js"; +import { getSettings } from "~/utils/parseSettings.js"; + +// Register handlebars helpers +handlebars.registerHelper("eq", function (a, b) { + return a === b; +}); + +handlebars.registerHelper( + "findFirst", + function (this: any, array, predicate, options) { + if (!array || !Array.isArray(array)) return options.inverse(this); + + for (let i = 0; i < array.length; i++) { + const item = array[i]; + if (predicate === "fm" && item.type === "fm") { + return options.fn(item); + } + } + return options.inverse(this); + } +); + +export function buildHandlebarsData(schemaName?: string) { + const proofkit = getSettings(); + const shadcn = getShadcnConfig(); + + return { + proofkit, + shadcn, + schemaName, + }; +} + +export async function randerHandlebarsToFile( + file: TemplateFile, + data: ReturnType +) { + const inputPath = getFilePath(file, data); + const rawTemplate = await fs.readFile(inputPath, "utf8"); + const template = handlebars.compile(rawTemplate); + + const rendered = template(data); + await fs.writeFile(inputPath, rendered); +} + +export function getFilePath( + file: TemplateFile, + data: ReturnType +): string { + const thePath = file.sourceFileName; + + if (file.destinationPath) return file.destinationPath; + + const cwd = state.projectDir ?? process.cwd(); + const { shadcn } = data; + + // Create a mapping between registry types and their corresponding shadcn config aliases + const typeToAliasMap: Record = { + "registry:lib": shadcn?.aliases?.lib || shadcn?.aliases?.utils, + "registry:component": shadcn?.aliases?.components, + "registry:ui": shadcn?.aliases?.ui || shadcn?.aliases?.components, + "registry:hook": shadcn?.aliases?.hooks, + // These types don't have direct aliases, so we use fallback paths + "registry:file": "src", + "registry:page": "src/app", + "registry:block": shadcn?.aliases?.components + ? shadcn.aliases.components.startsWith("@/") + ? `${shadcn.aliases.components.replace("@/", "src/")}/blocks` + : `src/${shadcn.aliases.components}/blocks` + : "src/components/blocks", + "registry:theme": "src/theme", + "registry:style": "src/styles", + }; + + const aliasPath = typeToAliasMap[file.type]; + + if (aliasPath) { + // Handle @/ prefix which represents the src directory + if (aliasPath.startsWith("@/")) { + const resolvedPath = aliasPath.replace("@/", "src/"); + return path.join(cwd, resolvedPath, thePath); + } + // If the alias starts with a path separator or contains src/, treat it as a relative path from cwd + else if (aliasPath.startsWith("/") || aliasPath.includes("src/")) { + return path.join(cwd, aliasPath, thePath); + } + // Otherwise, treat it as an alias that should be resolved relative to src/ + else { + return path.join(cwd, "src", aliasPath, thePath); + } + } + + // Fallback to hardcoded paths for unsupported types + switch (file.type) { + case "registry:lib": + return path.join(cwd, "src", "lib", thePath); + case "registry:file": + return path.join(cwd, "src", thePath); + case "registry:page": + return path.join(cwd, "src", "app", thePath); + case "registry:block": + return path.join(cwd, "src", "components", "blocks", thePath); + case "registry:component": + return path.join(cwd, "src", "components", thePath); + case "registry:ui": + return path.join(cwd, "src", "components", thePath); + case "registry:hook": + return path.join(cwd, "src", "hooks", thePath); + case "registry:theme": + return path.join(cwd, "src", "theme", thePath); + case "registry:style": + return path.join(cwd, "src", "styles", thePath); + default: + // default to source file name + return thePath; + } +} diff --git a/packages/cli/src/cli/add/registry/preflight.ts b/packages/cli/src/cli/add/registry/preflight.ts index a06f87ec..0c046b89 100644 --- a/packages/cli/src/cli/add/registry/preflight.ts +++ b/packages/cli/src/cli/add/registry/preflight.ts @@ -1,16 +1,21 @@ -import fs from "fs-extra"; import path from "path"; +import fs from "fs-extra"; + import { stealthInit } from "~/helpers/stealth-init.js"; import { state } from "~/state.js"; export async function preflightAddCommand() { - const cwd = state.projectDir ?? process.cwd(); - // make sure shadcn is installed, throw if not - const shadcnInstalled = await fs.pathExists(path.join(cwd, "components.json")); - if (!shadcnInstalled) { - throw new Error("Shadcn is not installed. Please run `pnpm dlx shadcn@latest init` to install it."); - } + const cwd = state.projectDir ?? process.cwd(); + // make sure shadcn is installed, throw if not + const shadcnInstalled = await fs.pathExists( + path.join(cwd, "components.json") + ); + if (!shadcnInstalled) { + throw new Error( + "Shadcn is not installed. Please run `pnpm dlx shadcn@latest init` to install it." + ); + } - // if proofkit is not inited, try to stealth init - await stealthInit(); -} \ No newline at end of file + // if proofkit is not inited, try to stealth init + await stealthInit(); +} diff --git a/packages/cli/src/helpers/shadcn-cli.ts b/packages/cli/src/helpers/shadcn-cli.ts index 6319d198..d588827e 100644 --- a/packages/cli/src/helpers/shadcn-cli.ts +++ b/packages/cli/src/helpers/shadcn-cli.ts @@ -1,12 +1,12 @@ import fs from "fs"; import path from "path"; +import { execa } from "execa"; import { DEFAULT_REGISTRY_URL } from "~/consts.js"; import { state } from "~/state.js"; import { logger } from "~/utils/logger.js"; import { getSettings } from "~/utils/parseSettings.js"; import { runExecCommand } from "./installDependencies.js"; -import { execa } from "execa"; export async function shadcnInstall( components: string | string[], @@ -15,7 +15,7 @@ export async function shadcnInstall( const componentsArray = Array.isArray(components) ? components : [components]; const command = ["shadcn@latest", "add", ...componentsArray, "--overwrite"]; // Use execa to run the shadcn add command directly - + try { await execa("pnpm", ["dlx", ...command], { stdio: "inherit", diff --git a/packages/cli/src/helpers/stealth-init.ts b/packages/cli/src/helpers/stealth-init.ts index 3d6637fe..47a4a84a 100644 --- a/packages/cli/src/helpers/stealth-init.ts +++ b/packages/cli/src/helpers/stealth-init.ts @@ -1,23 +1,17 @@ - - - - - - import fs from "fs-extra"; -import { defaultSettings } from "~/utils/parseSettings.js"; +import { defaultSettings } from "~/utils/parseSettings.js"; /** * Used to add a proofkit.json file to an existing project */ export async function stealthInit() { - // check if proofkit.json exists - const proofkitJson = await fs.pathExists("proofkit.json"); - if (proofkitJson) { - return; - } - - // create proofkit.json - await fs.writeJson("proofkit.json", defaultSettings); -} \ No newline at end of file + // check if proofkit.json exists + const proofkitJson = await fs.pathExists("proofkit.json"); + if (proofkitJson) { + return; + } + + // create proofkit.json + await fs.writeJson("proofkit.json", defaultSettings); +} diff --git a/packages/cli/src/utils/parseSettings.ts b/packages/cli/src/utils/parseSettings.ts index bc0a9cdd..d8912c39 100644 --- a/packages/cli/src/utils/parseSettings.ts +++ b/packages/cli/src/utils/parseSettings.ts @@ -68,7 +68,10 @@ const settingsSchema = z.object({ registryTemplates: z.array(z.string()).default([]), }); -export const defaultSettings = settingsSchema.parse({ auth: { type: "none" },ui:"shadcn" }); +export const defaultSettings = settingsSchema.parse({ + auth: { type: "none" }, + ui: "shadcn", +}); let settings: Settings | undefined; export const getSettings = () => { diff --git a/packages/registry/lib/types.ts b/packages/registry/lib/types.ts index 1a356909..2af89daa 100644 --- a/packages/registry/lib/types.ts +++ b/packages/registry/lib/types.ts @@ -31,11 +31,13 @@ export const templateFileSchema = z.discriminatedUnion("type", [ // The destination path in a consumer project, relative to project root destinationPath: z.string().optional(), type: registryTypeSchema.extract(["registry:file", "registry:page"]), + handlebars: z.boolean().optional(), }), z.object({ sourceFileName: z.string(), destinationPath: z.string().optional(), type: registryTypeSchema.exclude(["registry:file", "registry:page"]), + handlebars: z.boolean().optional(), }), ]); diff --git a/packages/registry/lib/utils.ts b/packages/registry/lib/utils.ts index f426e73c..454bfdf8 100644 --- a/packages/registry/lib/utils.ts +++ b/packages/registry/lib/utils.ts @@ -172,10 +172,14 @@ export async function getStaticComponent( const files: ShadcnFilesUnion = await Promise.all( meta.files.map(async (file) => { + const sourceFile = file.handlebars + ? file.sourceFileName.replace(/\.[^/.]+$/, ".hbs") + : file.sourceFileName; + const contentPath = path.join( templatesPath, normalized, - file.sourceFileName, + sourceFile, ); const content = await fs.readFile(contentPath, "utf-8"); diff --git a/packages/registry/lib/validator.ts b/packages/registry/lib/validator.ts index ff7242c5..6420f461 100644 --- a/packages/registry/lib/validator.ts +++ b/packages/registry/lib/validator.ts @@ -42,7 +42,13 @@ export function validateTemplateMetadata( const validatedMeta = validationResult.data; // Validate that declared files actually exist - const declaredFiles = validatedMeta.files.map((f) => f.sourceFileName); + const declaredFiles = validatedMeta.files.map((f) => { + if (f.handlebars) { + // Replace the file extension with .hbs for existence check + return f.sourceFileName.replace(/\.[^/.]+$/, ".hbs"); + } + return f.sourceFileName; + }); for (const declaredFile of declaredFiles) { const filePath = path.resolve(context.templateDir, declaredFile); diff --git a/packages/registry/templates/better-auth/_meta.ts b/packages/registry/templates/better-auth/_meta.ts index 3cbe5d71..7f1c7f14 100644 --- a/packages/registry/templates/better-auth/_meta.ts +++ b/packages/registry/templates/better-auth/_meta.ts @@ -36,6 +36,7 @@ export const meta: TemplateMetadata = { { sourceFileName: "auth.ts", type: "registry:lib", + handlebars: true, }, { sourceFileName: "auth-client.ts", diff --git a/packages/registry/templates/better-auth/auth.ts b/packages/registry/templates/better-auth/auth.hbs similarity index 80% rename from packages/registry/templates/better-auth/auth.ts rename to packages/registry/templates/better-auth/auth.hbs index 5174797f..0dbe3e8e 100644 --- a/packages/registry/templates/better-auth/auth.ts +++ b/packages/registry/templates/better-auth/auth.hbs @@ -1,7 +1,7 @@ import { FileMakerAdapter } from "@proofkit/better-auth"; import { betterAuth } from "better-auth"; import { nextCookies } from "better-auth/next-js"; -import { env } from "@/registry/lib/env"; +import { env } from "@/registry/default/lib/env"; import { render } from "@react-email/components"; import { GenericEmail } from "@/emails/generic"; @@ -9,11 +9,20 @@ export const auth = betterAuth({ // database database: FileMakerAdapter({ debugLogs: true, + {{#findFirst proofkit.dataSources "fm"}} odata: { - serverUrl: env.FM_SERVER, - auth: { apiKey: env.OTTO_API_KEY }, - database: env.FM_DATABASE, + serverUrl: env.{{envNames.server}}, + auth: { apiKey: env.{{envNames.apiKey}} }, + database: env.{{envNames.database}}, }, + {{else}} + // TODO: Add your FileMaker data source here + // odata: { + // serverUrl: env.FM_SERVER, + // auth: { apiKey: env.OTTO_API_KEY }, + // database: env.FM_DATABASE, + // }, + {{/findFirst}} }), emailAndPassword: { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 57032458..d85f281e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -353,6 +353,9 @@ importers: gradient-string: specifier: ^2.0.2 version: 2.0.2 + handlebars: + specifier: ^4.7.8 + version: 4.7.8 jiti: specifier: ^1.21.7 version: 1.21.7 @@ -5755,6 +5758,11 @@ packages: resolution: {integrity: sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==} engines: {node: '>=6.0'} + handlebars@4.7.8: + resolution: {integrity: sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==} + engines: {node: '>=0.4.7'} + hasBin: true + hanji@0.0.5: resolution: {integrity: sha512-Abxw1Lq+TnYiL4BueXqMau222fPSPMFtya8HdpWsz/xVAhifXou71mPh/kY2+08RgFcVccjG3uZHs6K5HAe3zw==} @@ -6828,6 +6836,9 @@ packages: resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} engines: {node: '>= 0.6'} + neo-async@2.6.2: + resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} + neverthrow@8.2.0: resolution: {integrity: sha512-kOCT/1MCPAxY5iUV3wytNFUMUolzuwd/VF/1KCx7kf6CutrOsTie+84zTGTpgQycjvfLdBBdvBvFLqFD2c0wkQ==} engines: {node: '>=18'} @@ -8318,6 +8329,11 @@ packages: ufo@1.6.1: resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==} + uglify-js@3.19.3: + resolution: {integrity: sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==} + engines: {node: '>=0.8.0'} + hasBin: true + uint8array-extras@1.4.0: resolution: {integrity: sha512-ZPtzy0hu4cZjv3z5NW9gfKnNLjoz4y6uv4HlelAjDK7sY/xOkKZv9xK/WQpcsBB3jEybChz9DPC2U/+cusjJVQ==} engines: {node: '>=18'} @@ -14096,6 +14112,15 @@ snapshots: section-matter: 1.0.0 strip-bom-string: 1.0.0 + handlebars@4.7.8: + dependencies: + minimist: 1.2.8 + neo-async: 2.6.2 + source-map: 0.6.1 + wordwrap: 1.0.0 + optionalDependencies: + uglify-js: 3.19.3 + hanji@0.0.5: dependencies: lodash.throttle: 4.1.1 @@ -15425,6 +15450,8 @@ snapshots: negotiator@1.0.0: {} + neo-async@2.6.2: {} + neverthrow@8.2.0: optionalDependencies: '@rollup/rollup-linux-x64-gnu': 4.40.2 @@ -17167,6 +17194,9 @@ snapshots: ufo@1.6.1: {} + uglify-js@3.19.3: + optional: true + uint8array-extras@1.4.0: {} unbox-primitive@1.1.0: From 2f9cdd8e818659f11f52f0aca338161de88c7f3f Mon Sep 17 00:00:00 2001 From: Eric Luce <37158449+eluce2@users.noreply.github.com> Date: Sat, 23 Aug 2025 12:12:35 -0500 Subject: [PATCH 2/6] simplify settings --- packages/cli/src/cli/add/auth.ts | 3 ++ packages/cli/src/cli/add/index.ts | 4 +- .../src/cli/add/page/post-install/table.ts | 6 ++- packages/cli/src/cli/init.ts | 16 +++++-- packages/cli/src/generators/auth.ts | 3 ++ packages/cli/src/generators/tanstack-query.ts | 1 + packages/cli/src/installers/react-email.ts | 1 + packages/cli/src/upgrades/index.ts | 8 +++- packages/cli/src/utils/parseSettings.ts | 47 ++++++++++++------- 9 files changed, 66 insertions(+), 23 deletions(-) diff --git a/packages/cli/src/cli/add/auth.ts b/packages/cli/src/cli/add/auth.ts index 9e5208dd..ffa1912f 100644 --- a/packages/cli/src/cli/add/auth.ts +++ b/packages/cli/src/cli/add/auth.ts @@ -105,6 +105,9 @@ export const makeAddAuthCommand = () => { .action(async () => { const settings = getSettings(); + if (settings.ui === "shadcn") { + throw new Error("Shadcn projects should add auth using the template registry"); + } if (settings.auth.type !== "none") { throw new Error("Auth already exists"); } diff --git a/packages/cli/src/cli/add/index.ts b/packages/cli/src/cli/add/index.ts index 24ca85ed..4d39ea0f 100644 --- a/packages/cli/src/cli/add/index.ts +++ b/packages/cli/src/cli/add/index.ts @@ -33,6 +33,8 @@ export const runAdd = async ( ensureProofKitProject({ commandName: "add" }); const settings = getSettings(); + + const addType = abortIfCancel( await p.select({ message: "What do you want to add to your project?", @@ -53,7 +55,7 @@ export const runAdd = async ( }, ] : []), - ...(settings.auth.type === "none" && settings.appType === "browser" + ...(settings.ui === "shadcn" ? [] : settings.auth.type === "none" && settings.appType === "browser" ? [{ label: "Auth", value: "auth" }] : []), ], diff --git a/packages/cli/src/cli/add/page/post-install/table.ts b/packages/cli/src/cli/add/page/post-install/table.ts index d358f413..d9fed507 100644 --- a/packages/cli/src/cli/add/page/post-install/table.ts +++ b/packages/cli/src/cli/add/page/post-install/table.ts @@ -37,7 +37,11 @@ export const postInstallTable: TPostInstallFn = async ({ dataSourceName: dataSource.name, }); - const { auth } = getSettings(); + const settings = getSettings(); + if (settings.ui === "shadcn") { + return; + } + const auth = settings.auth; const substitutions = { __SOURCE_NAME__: dataSource.name, diff --git a/packages/cli/src/cli/init.ts b/packages/cli/src/cli/init.ts index 6893484b..9906d1e5 100644 --- a/packages/cli/src/cli/init.ts +++ b/packages/cli/src/cli/init.ts @@ -246,16 +246,26 @@ export const runInit = async (name?: string, opts?: CliFlags) => { }); // Ensure proofkit.json exists with initial settings including ui - const initialSettings: Settings = { + const initialSettings: Settings = state.ui==="mantine"?{ appType: state.appType ?? "browser", - ui: (state.ui as "shadcn" | "mantine") ?? "shadcn", + ui: "mantine", auth: { type: "none" }, envFile: ".env", dataSources: [], tanstackQuery: false, replacedMainPage: false, appliedUpgrades: ["cursorRules"], - }; + reactEmail: false, + reactEmailServer: false, + registryTemplates: [], + }:{ + appType: state.appType ?? "browser", + ui: "shadcn", + envFile: ".env", + dataSources: [], + replacedMainPage: false, + registryTemplates: [], + } const { registryUrl } = setSettings(initialSettings); // for webviewer apps FM is required, so don't ask diff --git a/packages/cli/src/generators/auth.ts b/packages/cli/src/generators/auth.ts index f00e28c1..1eafd69a 100644 --- a/packages/cli/src/generators/auth.ts +++ b/packages/cli/src/generators/auth.ts @@ -26,6 +26,9 @@ export async function addAuth({ noInstall?: boolean; }) { const settings = getSettings(); + if (settings.ui === "shadcn") { + throw new Error("Shadcn projects should add auth using the template registry"); + } if (settings.auth.type !== "none") { throw new Error("Auth already exists"); } else if ( diff --git a/packages/cli/src/generators/tanstack-query.ts b/packages/cli/src/generators/tanstack-query.ts index 6f85cfdd..45b1983f 100644 --- a/packages/cli/src/generators/tanstack-query.ts +++ b/packages/cli/src/generators/tanstack-query.ts @@ -11,6 +11,7 @@ import { formatAndSaveSourceFiles, getNewProject } from "~/utils/ts-morph.js"; export async function injectTanstackQuery(args?: { project?: Project }) { const projectDir = state.projectDir; const settings = getSettings(); + if (settings.ui === "shadcn") return false; if (settings.tanstackQuery) return false; addPackageDependency({ diff --git a/packages/cli/src/installers/react-email.ts b/packages/cli/src/installers/react-email.ts index 594192a7..888ee287 100644 --- a/packages/cli/src/installers/react-email.ts +++ b/packages/cli/src/installers/react-email.ts @@ -26,6 +26,7 @@ export async function installReactEmail({ // Exit early if already installed const settings = getSettings(); + if (settings.ui === "shadcn") return false; if (settings.reactEmail) return false; // Ensure emails directory exists diff --git a/packages/cli/src/upgrades/index.ts b/packages/cli/src/upgrades/index.ts index 51dffac3..58f3e211 100644 --- a/packages/cli/src/upgrades/index.ts +++ b/packages/cli/src/upgrades/index.ts @@ -33,6 +33,9 @@ export type UpgradeKeys = (typeof availableUpgrades)[number]["key"]; export function checkForAvailableUpgrades() { const settings = getSettings(); + if (settings.ui === "shadcn") { + return []; + } const appliedUpgrades = settings.appliedUpgrades; @@ -51,13 +54,16 @@ export function checkForAvailableUpgrades() { export async function runAllAvailableUpgrades() { const upgrades = checkForAvailableUpgrades(); + const settings = getSettings(); + if (settings.ui === "shadcn") return + for (const upgrade of upgrades) { const upgradeFunction = availableUpgrades.find( (u) => u.key === upgrade.key )?.function; if (upgradeFunction) { await upgradeFunction(); - const appliedUpgrades = getSettings().appliedUpgrades; + const appliedUpgrades = settings.appliedUpgrades; mergeSettings({ appliedUpgrades: [...appliedUpgrades, upgrade.key], }); diff --git a/packages/cli/src/utils/parseSettings.ts b/packages/cli/src/utils/parseSettings.ts index d8912c39..155cf053 100644 --- a/packages/cli/src/utils/parseSettings.ts +++ b/packages/cli/src/utils/parseSettings.ts @@ -51,22 +51,33 @@ export const appTypes = ["browser", "webviewer"] as const; export const uiTypes = ["shadcn", "mantine"] as const; export type Ui = (typeof uiTypes)[number]; -const settingsSchema = z.object({ - appType: z.enum(appTypes).default("browser"), - ui: z.enum(uiTypes).default("mantine"), // default if the ui key is missing - auth: authSchema, - envFile: z.string().default(".env"), - dataSources: z.array(dataSourceSchema).default([]), - tanstackQuery: z.boolean().catch(false), - replacedMainPage: z.boolean().catch(false), - // Whether React Email scaffolding has been installed - reactEmail: z.boolean().catch(false), - // Whether provider-specific server email sender files have been installed - reactEmailServer: z.boolean().catch(false), - appliedUpgrades: z.array(z.string()).default([]), - registryUrl: z.url().optional(), - registryTemplates: z.array(z.string()).default([]), -}); +const settingsSchema = z.discriminatedUnion("ui", [ + z.object({ + ui: z.literal("mantine"), + appType: z.enum(appTypes).default("browser"), + auth: authSchema, + envFile: z.string().default(".env"), + dataSources: z.array(dataSourceSchema).default([]), + tanstackQuery: z.boolean().catch(false), + replacedMainPage: z.boolean().catch(false), + // Whether React Email scaffolding has been installed + reactEmail: z.boolean().catch(false), + // Whether provider-specific server email sender files have been installed + reactEmailServer: z.boolean().catch(false), + appliedUpgrades: z.array(z.string()).default([]), + registryUrl: z.url().optional(), + registryTemplates: z.array(z.string()).default([]), + }), + z.object({ + ui: z.literal("shadcn"), + appType: z.enum(appTypes).default("browser"), + envFile: z.string().default(".env"), + dataSources: z.array(dataSourceSchema).default([]), + replacedMainPage: z.boolean().catch(false), + registryUrl: z.url().optional(), + registryTemplates: z.array(z.string()).default([]), + }), +]); export const defaultSettings = settingsSchema.parse({ auth: { type: "none" }, @@ -96,7 +107,9 @@ export type Settings = z.infer; export function mergeSettings(_settings: Partial) { const settings = getSettings(); - setSettings({ ...settings, ..._settings }); + const merged = { ...settings, ..._settings }; + const validated = settingsSchema.parse(merged); + setSettings(validated); } export function setSettings(_settings: Settings) { From eb3887a242bfc0bb364661aa1449ffd3390515aa Mon Sep 17 00:00:00 2001 From: Eric Luce <37158449+eluce2@users.noreply.github.com> Date: Sun, 24 Aug 2025 08:02:42 -0500 Subject: [PATCH 3/6] better cli --- .../add/registry/postInstall/handlebars.ts | 55 ++- packages/cli/src/helpers/shadcn-cli.ts | 2 +- packages/cli/src/installers/envVars.ts | 30 +- packages/cli/src/utils/addToEnvs.ts | 3 +- .../template/nextjs-shadcn/.claude/CLAUDE.md | 327 +++++++++++++++++ .../nextjs-shadcn/.cursor/rules/ultracite.mdc | 333 ++++++++++++++++++ .../nextjs-shadcn/.vscode/settings.json | 35 ++ .../cli/template/nextjs-shadcn/biome.json | 43 +++ .../template/nextjs-shadcn/components.json | 10 +- .../cli/template/nextjs-shadcn/next.config.ts | 10 +- .../cli/template/nextjs-shadcn/package.json | 53 ++- .../template/nextjs-shadcn/postcss.config.cjs | 5 - .../template/nextjs-shadcn/postcss.config.mjs | 5 + .../nextjs-shadcn/src/app/(main)/page.tsx | 74 ++-- .../src/{config/theme => app}/globals.css | 136 ++++--- .../template/nextjs-shadcn/src/app/layout.tsx | 31 +- .../nextjs-shadcn/src/components/AppLogo.tsx | 6 +- .../nextjs-shadcn/src/{config => lib}/env.ts | 1 - .../template/nextjs-shadcn/src/lib/utils.ts | 6 + .../nextjs-shadcn/src/server/safe-action.ts | 3 - .../src/utils/notification-helpers.ts | 16 - .../nextjs-shadcn/src/utils/styles.ts | 6 - .../cli/template/nextjs-shadcn/tsconfig.json | 26 +- 23 files changed, 1010 insertions(+), 206 deletions(-) create mode 100644 packages/cli/template/nextjs-shadcn/.claude/CLAUDE.md create mode 100644 packages/cli/template/nextjs-shadcn/.cursor/rules/ultracite.mdc create mode 100644 packages/cli/template/nextjs-shadcn/.vscode/settings.json create mode 100644 packages/cli/template/nextjs-shadcn/biome.json delete mode 100644 packages/cli/template/nextjs-shadcn/postcss.config.cjs create mode 100644 packages/cli/template/nextjs-shadcn/postcss.config.mjs rename packages/cli/template/nextjs-shadcn/src/{config/theme => app}/globals.css (72%) rename packages/cli/template/nextjs-shadcn/src/{config => lib}/env.ts (78%) create mode 100644 packages/cli/template/nextjs-shadcn/src/lib/utils.ts delete mode 100644 packages/cli/template/nextjs-shadcn/src/server/safe-action.ts delete mode 100644 packages/cli/template/nextjs-shadcn/src/utils/notification-helpers.ts delete mode 100644 packages/cli/template/nextjs-shadcn/src/utils/styles.ts diff --git a/packages/cli/src/cli/add/registry/postInstall/handlebars.ts b/packages/cli/src/cli/add/registry/postInstall/handlebars.ts index d5788ec7..26b6d41b 100644 --- a/packages/cli/src/cli/add/registry/postInstall/handlebars.ts +++ b/packages/cli/src/cli/add/registry/postInstall/handlebars.ts @@ -5,7 +5,8 @@ import handlebars from "handlebars"; import { getShadcnConfig } from "~/helpers/shadcn-cli.js"; import { state } from "~/state.js"; -import { getSettings } from "~/utils/parseSettings.js"; +import { DataSource, getSettings } from "~/utils/parseSettings.js"; +import { getClientSuffix, getFieldNamesForSchema } from "~/generators/fmdapi.js"; // Register handlebars helpers handlebars.registerHelper("eq", function (a, b) { @@ -27,14 +28,62 @@ handlebars.registerHelper( } ); -export function buildHandlebarsData(schemaName?: string) { +type DataSourceForTemplate = { + dataSource: DataSource; + schemaName: string; +} + +const commonFieldNamesToExclude = [ + "id", + "pk", + "createdat", + "updatedat", + "primarykey", + "createdby", + "modifiedby", + "creationtimestamp", + "modificationtimestamp", +]; + +function filterOutCommonFieldNames(fieldNames: string[]): string[] { + return fieldNames.filter( + (fieldName) => + !commonFieldNamesToExclude.includes(fieldName.toLowerCase()) || + fieldName.startsWith("_") + ); +} + + +function buildDataSourceData(args: DataSourceForTemplate) { + const { dataSource, schemaName } = args; + + const clientSuffix = getClientSuffix({ + projectDir: state.projectDir??process.cwd(), + dataSourceName: dataSource.name, + }); + + const allFieldNames = getFieldNamesForSchema({ + schemaName, + dataSourceName: dataSource.name, + }).filter(Boolean) as string[]; + + return { + sourceName: dataSource.name, + schemaName, + clientSuffix, + allFieldNames, + fieldNames: filterOutCommonFieldNames(allFieldNames) + } +} + +export function buildHandlebarsData(args?: DataSourceForTemplate) { const proofkit = getSettings(); const shadcn = getShadcnConfig(); return { proofkit, shadcn, - schemaName, + schema: args ? buildDataSourceData(args) : undefined, }; } diff --git a/packages/cli/src/helpers/shadcn-cli.ts b/packages/cli/src/helpers/shadcn-cli.ts index d588827e..147f72b2 100644 --- a/packages/cli/src/helpers/shadcn-cli.ts +++ b/packages/cli/src/helpers/shadcn-cli.ts @@ -19,7 +19,7 @@ export async function shadcnInstall( try { await execa("pnpm", ["dlx", ...command], { stdio: "inherit", - cwd: process.cwd(), + cwd: state.projectDir ?? process.cwd(), }); } catch (error) { logger.error(`Failed to run shadcn add: ${error}`); diff --git a/packages/cli/src/installers/envVars.ts b/packages/cli/src/installers/envVars.ts index 726bc0d1..f75600bf 100644 --- a/packages/cli/src/installers/envVars.ts +++ b/packages/cli/src/installers/envVars.ts @@ -3,14 +3,17 @@ import fs from "fs-extra"; import { type Installer } from "~/installers/index.js"; import { state } from "~/state.js"; +import { logger } from "~/utils/logger.js"; export type FMAuthKeys = | { username: string; password: string } | { ottoApiKey: string }; export const initEnvFile: Installer = () => { + const envFilePath = findT3EnvFile(false) ?? `./src/config/env.ts`; + const envContent = ` -# When adding additional environment variables, the schema in "/src/config/env.ts" +# When adding additional environment variables, the schema in "${envFilePath}" # should be updated accordingly. ` @@ -21,3 +24,28 @@ export const initEnvFile: Installer = () => { fs.writeFileSync(envDest, envContent, "utf-8"); }; + +export function findT3EnvFile(): string +export function findT3EnvFile(throwIfNotFound: false): string | null +export function findT3EnvFile(throwIfNotFound: true): string +export function findT3EnvFile(throwIfNotFound?: boolean): string | null { + const possiblePaths = [ + `/src/config/env.ts`, + `/src/lib/env.ts`, + `/src/env.ts`, + ] + + for (const testPath of possiblePaths) { + const fullPath = path.join(state.projectDir, testPath); + if (fs.existsSync(fullPath)) { + return fullPath; + } + } + + if (throwIfNotFound === false) { + return null; + } + + logger.warn(`Could not find the T3 env files. Run "proofkit add utils/t3-env" to initilziate it`) + throw new Error("T3 env file not found") +} \ No newline at end of file diff --git a/packages/cli/src/utils/addToEnvs.ts b/packages/cli/src/utils/addToEnvs.ts index 7222a07d..610dfda2 100644 --- a/packages/cli/src/utils/addToEnvs.ts +++ b/packages/cli/src/utils/addToEnvs.ts @@ -3,6 +3,7 @@ import fs from "fs-extra"; import { SyntaxKind, type Project } from "ts-morph"; import { formatAndSaveSourceFiles, getNewProject } from "./ts-morph.js"; +import { findT3EnvFile } from "~/installers/envVars.js"; interface EnvSchema { name: string; @@ -24,7 +25,7 @@ export async function addToEnv({ envs: EnvSchema[]; envFileDescription?: string; }) { - const envSchemaFile = path.join(projectDir, "src/config/env.ts"); + const envSchemaFile = findT3EnvFile() const project = args.project ?? getNewProject(projectDir); const schemaFile = project.addSourceFileAtPath(envSchemaFile); diff --git a/packages/cli/template/nextjs-shadcn/.claude/CLAUDE.md b/packages/cli/template/nextjs-shadcn/.claude/CLAUDE.md new file mode 100644 index 00000000..869eaac0 --- /dev/null +++ b/packages/cli/template/nextjs-shadcn/.claude/CLAUDE.md @@ -0,0 +1,327 @@ +# Project Context +Ultracite enforces strict type safety, accessibility standards, and consistent code quality for JavaScript/TypeScript projects using Biome's lightning-fast formatter and linter. + +## Key Principles +- Zero configuration required +- Subsecond performance +- Maximum type safety +- AI-friendly code generation + +## Before Writing Code +1. Analyze existing patterns in the codebase +2. Consider edge cases and error scenarios +3. Follow the rules below strictly +4. Validate accessibility requirements + +## Rules + +### Accessibility (a11y) +- Don't use `accessKey` attribute on any HTML element. +- Don't set `aria-hidden="true"` on focusable elements. +- Don't add ARIA roles, states, and properties to elements that don't support them. +- Don't use distracting elements like `` or ``. +- Only use the `scope` prop on `` elements. +- Don't assign non-interactive ARIA roles to interactive HTML elements. +- Make sure label elements have text content and are associated with an input. +- Don't assign interactive ARIA roles to non-interactive HTML elements. +- Don't assign `tabIndex` to non-interactive HTML elements. +- Don't use positive integers for `tabIndex` property. +- Don't include "image", "picture", or "photo" in img alt prop. +- Don't use explicit role property that's the same as the implicit/default role. +- Make static elements with click handlers use a valid role attribute. +- Always include a `title` element for SVG elements. +- Give all elements requiring alt text meaningful information for screen readers. +- Make sure anchors have content that's accessible to screen readers. +- Assign `tabIndex` to non-interactive HTML elements with `aria-activedescendant`. +- Include all required ARIA attributes for elements with ARIA roles. +- Make sure ARIA properties are valid for the element's supported roles. +- Always include a `type` attribute for button elements. +- Make elements with interactive roles and handlers focusable. +- Give heading elements content that's accessible to screen readers (not hidden with `aria-hidden`). +- Always include a `lang` attribute on the html element. +- Always include a `title` attribute for iframe elements. +- Accompany `onClick` with at least one of: `onKeyUp`, `onKeyDown`, or `onKeyPress`. +- Accompany `onMouseOver`/`onMouseOut` with `onFocus`/`onBlur`. +- Include caption tracks for audio and video elements. +- Use semantic elements instead of role attributes in JSX. +- Make sure all anchors are valid and navigable. +- Ensure all ARIA properties (`aria-*`) are valid. +- Use valid, non-abstract ARIA roles for elements with ARIA roles. +- Use valid ARIA state and property values. +- Use valid values for the `autocomplete` attribute on input elements. +- Use correct ISO language/country codes for the `lang` attribute. + +### Code Complexity and Quality +- Don't use consecutive spaces in regular expression literals. +- Don't use the `arguments` object. +- Don't use primitive type aliases or misleading types. +- Don't use the comma operator. +- Don't use empty type parameters in type aliases and interfaces. +- Don't write functions that exceed a given Cognitive Complexity score. +- Don't nest describe() blocks too deeply in test files. +- Don't use unnecessary boolean casts. +- Don't use unnecessary callbacks with flatMap. +- Use for...of statements instead of Array.forEach. +- Don't create classes that only have static members (like a static namespace). +- Don't use this and super in static contexts. +- Don't use unnecessary catch clauses. +- Don't use unnecessary constructors. +- Don't use unnecessary continue statements. +- Don't export empty modules that don't change anything. +- Don't use unnecessary escape sequences in regular expression literals. +- Don't use unnecessary fragments. +- Don't use unnecessary labels. +- Don't use unnecessary nested block statements. +- Don't rename imports, exports, and destructured assignments to the same name. +- Don't use unnecessary string or template literal concatenation. +- Don't use String.raw in template literals when there are no escape sequences. +- Don't use useless case statements in switch statements. +- Don't use ternary operators when simpler alternatives exist. +- Don't use useless `this` aliasing. +- Don't use any or unknown as type constraints. +- Don't initialize variables to undefined. +- Don't use the void operators (they're not familiar). +- Use arrow functions instead of function expressions. +- Use Date.now() to get milliseconds since the Unix Epoch. +- Use .flatMap() instead of map().flat() when possible. +- Use literal property access instead of computed property access. +- Don't use parseInt() or Number.parseInt() when binary, octal, or hexadecimal literals work. +- Use concise optional chaining instead of chained logical expressions. +- Use regular expression literals instead of the RegExp constructor when possible. +- Don't use number literal object member names that aren't base 10 or use underscore separators. +- Remove redundant terms from logical expressions. +- Use while loops instead of for loops when you don't need initializer and update expressions. +- Don't pass children as props. +- Don't reassign const variables. +- Don't use constant expressions in conditions. +- Don't use `Math.min` and `Math.max` to clamp values when the result is constant. +- Don't return a value from a constructor. +- Don't use empty character classes in regular expression literals. +- Don't use empty destructuring patterns. +- Don't call global object properties as functions. +- Don't declare functions and vars that are accessible outside their block. +- Make sure builtins are correctly instantiated. +- Don't use super() incorrectly inside classes. Also check that super() is called in classes that extend other constructors. +- Don't use variables and function parameters before they're declared. +- Don't use 8 and 9 escape sequences in string literals. +- Don't use literal numbers that lose precision. + +### React and JSX Best Practices +- Don't use the return value of React.render. +- Make sure all dependencies are correctly specified in React hooks. +- Make sure all React hooks are called from the top level of component functions. +- Don't forget key props in iterators and collection literals. +- Don't destructure props inside JSX components in Solid projects. +- Don't define React components inside other components. +- Don't use event handlers on non-interactive elements. +- Don't assign to React component props. +- Don't use both `children` and `dangerouslySetInnerHTML` props on the same element. +- Don't use dangerous JSX props. +- Don't use Array index in keys. +- Don't insert comments as text nodes. +- Don't assign JSX properties multiple times. +- Don't add extra closing tags for components without children. +- Use `<>...` instead of `...`. +- Watch out for possible "wrong" semicolons inside JSX elements. + +### Correctness and Safety +- Don't assign a value to itself. +- Don't return a value from a setter. +- Don't compare expressions that modify string case with non-compliant values. +- Don't use lexical declarations in switch clauses. +- Don't use variables that haven't been declared in the document. +- Don't write unreachable code. +- Make sure super() is called exactly once on every code path in a class constructor before this is accessed if the class has a superclass. +- Don't use control flow statements in finally blocks. +- Don't use optional chaining where undefined values aren't allowed. +- Don't have unused function parameters. +- Don't have unused imports. +- Don't have unused labels. +- Don't have unused private class members. +- Don't have unused variables. +- Make sure void (self-closing) elements don't have children. +- Don't return a value from a function with the return type 'void' +- Use isNaN() when checking for NaN. +- Make sure "for" loop update clauses move the counter in the right direction. +- Make sure typeof expressions are compared to valid values. +- Make sure generator functions contain yield. +- Don't use await inside loops. +- Don't use bitwise operators. +- Don't use expressions where the operation doesn't change the value. +- Make sure Promise-like statements are handled appropriately. +- Don't use __dirname and __filename in the global scope. +- Prevent import cycles. +- Don't use configured elements. +- Don't hardcode sensitive data like API keys and tokens. +- Don't let variable declarations shadow variables from outer scopes. +- Don't use the TypeScript directive @ts-ignore. +- Prevent duplicate polyfills from Polyfill.io. +- Don't use useless backreferences in regular expressions that always match empty strings. +- Don't use unnecessary escapes in string literals. +- Don't use useless undefined. +- Make sure getters and setters for the same property are next to each other in class and object definitions. +- Make sure object literals are declared consistently (defaults to explicit definitions). +- Use static Response methods instead of new Response() constructor when possible. +- Make sure switch-case statements are exhaustive. +- Make sure the `preconnect` attribute is used when using Google Fonts. +- Use `Array#{indexOf,lastIndexOf}()` instead of `Array#{findIndex,findLastIndex}()` when looking for the index of an item. +- Make sure iterable callbacks return consistent values. +- Use `with { type: "json" }` for JSON module imports. +- Use numeric separators in numeric literals. +- Use object spread instead of `Object.assign()` when constructing new objects. +- Always use the radix argument when using `parseInt()`. +- Make sure JSDoc comment lines start with a single asterisk, except for the first one. +- Include a description parameter for `Symbol()`. +- Don't use spread (`...`) syntax on accumulators. +- Don't use the `delete` operator. +- Don't access namespace imports dynamically. +- Don't use namespace imports. +- Declare regex literals at the top level. +- Don't use `target="_blank"` without `rel="noopener"`. + +### TypeScript Best Practices +- Don't use TypeScript enums. +- Don't export imported variables. +- Don't add type annotations to variables, parameters, and class properties that are initialized with literal expressions. +- Don't use TypeScript namespaces. +- Don't use non-null assertions with the `!` postfix operator. +- Don't use parameter properties in class constructors. +- Don't use user-defined types. +- Use `as const` instead of literal types and type annotations. +- Use either `T[]` or `Array` consistently. +- Initialize each enum member value explicitly. +- Use `export type` for types. +- Use `import type` for types. +- Make sure all enum members are literal values. +- Don't use TypeScript const enum. +- Don't declare empty interfaces. +- Don't let variables evolve into any type through reassignments. +- Don't use the any type. +- Don't misuse the non-null assertion operator (!) in TypeScript files. +- Don't use implicit any type on variable declarations. +- Don't merge interfaces and classes unsafely. +- Don't use overload signatures that aren't next to each other. +- Use the namespace keyword instead of the module keyword to declare TypeScript namespaces. + +### Style and Consistency +- Don't use global `eval()`. +- Don't use callbacks in asynchronous tests and hooks. +- Don't use negation in `if` statements that have `else` clauses. +- Don't use nested ternary expressions. +- Don't reassign function parameters. +- This rule lets you specify global variable names you don't want to use in your application. +- Don't use specified modules when loaded by import or require. +- Don't use constants whose value is the upper-case version of their name. +- Use `String.slice()` instead of `String.substr()` and `String.substring()`. +- Don't use template literals if you don't need interpolation or special-character handling. +- Don't use `else` blocks when the `if` block breaks early. +- Don't use yoda expressions. +- Don't use Array constructors. +- Use `at()` instead of integer index access. +- Follow curly brace conventions. +- Use `else if` instead of nested `if` statements in `else` clauses. +- Use single `if` statements instead of nested `if` clauses. +- Use `new` for all builtins except `String`, `Number`, and `Boolean`. +- Use consistent accessibility modifiers on class properties and methods. +- Use `const` declarations for variables that are only assigned once. +- Put default function parameters and optional function parameters last. +- Include a `default` clause in switch statements. +- Use the `**` operator instead of `Math.pow`. +- Use `for-of` loops when you need the index to extract an item from the iterated array. +- Use `node:assert/strict` over `node:assert`. +- Use the `node:` protocol for Node.js builtin modules. +- Use Number properties instead of global ones. +- Use assignment operator shorthand where possible. +- Use function types instead of object types with call signatures. +- Use template literals over string concatenation. +- Use `new` when throwing an error. +- Don't throw non-Error values. +- Use `String.trimStart()` and `String.trimEnd()` over `String.trimLeft()` and `String.trimRight()`. +- Use standard constants instead of approximated literals. +- Don't assign values in expressions. +- Don't use async functions as Promise executors. +- Don't reassign exceptions in catch clauses. +- Don't reassign class members. +- Don't compare against -0. +- Don't use labeled statements that aren't loops. +- Don't use void type outside of generic or return types. +- Don't use console. +- Don't use control characters and escape sequences that match control characters in regular expression literals. +- Don't use debugger. +- Don't assign directly to document.cookie. +- Use `===` and `!==`. +- Don't use duplicate case labels. +- Don't use duplicate class members. +- Don't use duplicate conditions in if-else-if chains. +- Don't use two keys with the same name inside objects. +- Don't use duplicate function parameter names. +- Don't have duplicate hooks in describe blocks. +- Don't use empty block statements and static blocks. +- Don't let switch clauses fall through. +- Don't reassign function declarations. +- Don't allow assignments to native objects and read-only global variables. +- Use Number.isFinite instead of global isFinite. +- Use Number.isNaN instead of global isNaN. +- Don't assign to imported bindings. +- Don't use irregular whitespace characters. +- Don't use labels that share a name with a variable. +- Don't use characters made with multiple code points in character class syntax. +- Make sure to use new and constructor properly. +- Don't use shorthand assign when the variable appears on both sides. +- Don't use octal escape sequences in string literals. +- Don't use Object.prototype builtins directly. +- Don't redeclare variables, functions, classes, and types in the same scope. +- Don't have redundant "use strict". +- Don't compare things where both sides are exactly the same. +- Don't let identifiers shadow restricted names. +- Don't use sparse arrays (arrays with holes). +- Don't use template literal placeholder syntax in regular strings. +- Don't use the then property. +- Don't use unsafe negation. +- Don't use var. +- Don't use with statements in non-strict contexts. +- Make sure async functions actually use await. +- Make sure default clauses in switch statements come last. +- Make sure to pass a message value when creating a built-in error. +- Make sure get methods always return a value. +- Use a recommended display strategy with Google Fonts. +- Make sure for-in loops include an if statement. +- Use Array.isArray() instead of instanceof Array. +- Make sure to use the digits argument with Number#toFixed(). +- Make sure to use the "use strict" directive in script files. + +### Next.js Specific Rules +- Don't use `` elements in Next.js projects. +- Don't use `` elements in Next.js projects. +- Don't import next/document outside of pages/_document.jsx in Next.js projects. +- Don't use the next/head module in pages/_document.js on Next.js projects. + +### Testing Best Practices +- Don't use export or module.exports in test files. +- Don't use focused tests. +- Make sure the assertion function, like expect, is placed inside an it() function call. +- Don't use disabled tests. + +## Common Tasks +- `npx ultracite init` - Initialize Ultracite in your project +- `npx ultracite format` - Format and fix code automatically +- `npx ultracite lint` - Check for issues without fixing + +## Example: Error Handling +```typescript +// ✅ Good: Comprehensive error handling +try { + const result = await fetchData(); + return { success: true, data: result }; +} catch (error) { + console.error('API call failed:', error); + return { success: false, error: error.message }; +} + +// ❌ Bad: Swallowing errors +try { + return await fetchData(); +} catch (e) { + console.log(e); +} +``` \ No newline at end of file diff --git a/packages/cli/template/nextjs-shadcn/.cursor/rules/ultracite.mdc b/packages/cli/template/nextjs-shadcn/.cursor/rules/ultracite.mdc new file mode 100644 index 00000000..98495535 --- /dev/null +++ b/packages/cli/template/nextjs-shadcn/.cursor/rules/ultracite.mdc @@ -0,0 +1,333 @@ +--- +description: Ultracite Rules - AI-Ready Formatter and Linter +globs: "**/*.{ts,tsx,js,jsx}" +alwaysApply: true +--- + +# Project Context +Ultracite enforces strict type safety, accessibility standards, and consistent code quality for JavaScript/TypeScript projects using Biome's lightning-fast formatter and linter. + +## Key Principles +- Zero configuration required +- Subsecond performance +- Maximum type safety +- AI-friendly code generation + +## Before Writing Code +1. Analyze existing patterns in the codebase +2. Consider edge cases and error scenarios +3. Follow the rules below strictly +4. Validate accessibility requirements + +## Rules + +### Accessibility (a11y) +- Don't use `accessKey` attribute on any HTML element. +- Don't set `aria-hidden="true"` on focusable elements. +- Don't add ARIA roles, states, and properties to elements that don't support them. +- Don't use distracting elements like `` or ``. +- Only use the `scope` prop on `` elements. +- Don't assign non-interactive ARIA roles to interactive HTML elements. +- Make sure label elements have text content and are associated with an input. +- Don't assign interactive ARIA roles to non-interactive HTML elements. +- Don't assign `tabIndex` to non-interactive HTML elements. +- Don't use positive integers for `tabIndex` property. +- Don't include "image", "picture", or "photo" in img alt prop. +- Don't use explicit role property that's the same as the implicit/default role. +- Make static elements with click handlers use a valid role attribute. +- Always include a `title` element for SVG elements. +- Give all elements requiring alt text meaningful information for screen readers. +- Make sure anchors have content that's accessible to screen readers. +- Assign `tabIndex` to non-interactive HTML elements with `aria-activedescendant`. +- Include all required ARIA attributes for elements with ARIA roles. +- Make sure ARIA properties are valid for the element's supported roles. +- Always include a `type` attribute for button elements. +- Make elements with interactive roles and handlers focusable. +- Give heading elements content that's accessible to screen readers (not hidden with `aria-hidden`). +- Always include a `lang` attribute on the html element. +- Always include a `title` attribute for iframe elements. +- Accompany `onClick` with at least one of: `onKeyUp`, `onKeyDown`, or `onKeyPress`. +- Accompany `onMouseOver`/`onMouseOut` with `onFocus`/`onBlur`. +- Include caption tracks for audio and video elements. +- Use semantic elements instead of role attributes in JSX. +- Make sure all anchors are valid and navigable. +- Ensure all ARIA properties (`aria-*`) are valid. +- Use valid, non-abstract ARIA roles for elements with ARIA roles. +- Use valid ARIA state and property values. +- Use valid values for the `autocomplete` attribute on input elements. +- Use correct ISO language/country codes for the `lang` attribute. + +### Code Complexity and Quality +- Don't use consecutive spaces in regular expression literals. +- Don't use the `arguments` object. +- Don't use primitive type aliases or misleading types. +- Don't use the comma operator. +- Don't use empty type parameters in type aliases and interfaces. +- Don't write functions that exceed a given Cognitive Complexity score. +- Don't nest describe() blocks too deeply in test files. +- Don't use unnecessary boolean casts. +- Don't use unnecessary callbacks with flatMap. +- Use for...of statements instead of Array.forEach. +- Don't create classes that only have static members (like a static namespace). +- Don't use this and super in static contexts. +- Don't use unnecessary catch clauses. +- Don't use unnecessary constructors. +- Don't use unnecessary continue statements. +- Don't export empty modules that don't change anything. +- Don't use unnecessary escape sequences in regular expression literals. +- Don't use unnecessary fragments. +- Don't use unnecessary labels. +- Don't use unnecessary nested block statements. +- Don't rename imports, exports, and destructured assignments to the same name. +- Don't use unnecessary string or template literal concatenation. +- Don't use String.raw in template literals when there are no escape sequences. +- Don't use useless case statements in switch statements. +- Don't use ternary operators when simpler alternatives exist. +- Don't use useless `this` aliasing. +- Don't use any or unknown as type constraints. +- Don't initialize variables to undefined. +- Don't use the void operators (they're not familiar). +- Use arrow functions instead of function expressions. +- Use Date.now() to get milliseconds since the Unix Epoch. +- Use .flatMap() instead of map().flat() when possible. +- Use literal property access instead of computed property access. +- Don't use parseInt() or Number.parseInt() when binary, octal, or hexadecimal literals work. +- Use concise optional chaining instead of chained logical expressions. +- Use regular expression literals instead of the RegExp constructor when possible. +- Don't use number literal object member names that aren't base 10 or use underscore separators. +- Remove redundant terms from logical expressions. +- Use while loops instead of for loops when you don't need initializer and update expressions. +- Don't pass children as props. +- Don't reassign const variables. +- Don't use constant expressions in conditions. +- Don't use `Math.min` and `Math.max` to clamp values when the result is constant. +- Don't return a value from a constructor. +- Don't use empty character classes in regular expression literals. +- Don't use empty destructuring patterns. +- Don't call global object properties as functions. +- Don't declare functions and vars that are accessible outside their block. +- Make sure builtins are correctly instantiated. +- Don't use super() incorrectly inside classes. Also check that super() is called in classes that extend other constructors. +- Don't use variables and function parameters before they're declared. +- Don't use 8 and 9 escape sequences in string literals. +- Don't use literal numbers that lose precision. + +### React and JSX Best Practices +- Don't use the return value of React.render. +- Make sure all dependencies are correctly specified in React hooks. +- Make sure all React hooks are called from the top level of component functions. +- Don't forget key props in iterators and collection literals. +- Don't destructure props inside JSX components in Solid projects. +- Don't define React components inside other components. +- Don't use event handlers on non-interactive elements. +- Don't assign to React component props. +- Don't use both `children` and `dangerouslySetInnerHTML` props on the same element. +- Don't use dangerous JSX props. +- Don't use Array index in keys. +- Don't insert comments as text nodes. +- Don't assign JSX properties multiple times. +- Don't add extra closing tags for components without children. +- Use `<>...` instead of `...`. +- Watch out for possible "wrong" semicolons inside JSX elements. + +### Correctness and Safety +- Don't assign a value to itself. +- Don't return a value from a setter. +- Don't compare expressions that modify string case with non-compliant values. +- Don't use lexical declarations in switch clauses. +- Don't use variables that haven't been declared in the document. +- Don't write unreachable code. +- Make sure super() is called exactly once on every code path in a class constructor before this is accessed if the class has a superclass. +- Don't use control flow statements in finally blocks. +- Don't use optional chaining where undefined values aren't allowed. +- Don't have unused function parameters. +- Don't have unused imports. +- Don't have unused labels. +- Don't have unused private class members. +- Don't have unused variables. +- Make sure void (self-closing) elements don't have children. +- Don't return a value from a function with the return type 'void' +- Use isNaN() when checking for NaN. +- Make sure "for" loop update clauses move the counter in the right direction. +- Make sure typeof expressions are compared to valid values. +- Make sure generator functions contain yield. +- Don't use await inside loops. +- Don't use bitwise operators. +- Don't use expressions where the operation doesn't change the value. +- Make sure Promise-like statements are handled appropriately. +- Don't use __dirname and __filename in the global scope. +- Prevent import cycles. +- Don't use configured elements. +- Don't hardcode sensitive data like API keys and tokens. +- Don't let variable declarations shadow variables from outer scopes. +- Don't use the TypeScript directive @ts-ignore. +- Prevent duplicate polyfills from Polyfill.io. +- Don't use useless backreferences in regular expressions that always match empty strings. +- Don't use unnecessary escapes in string literals. +- Don't use useless undefined. +- Make sure getters and setters for the same property are next to each other in class and object definitions. +- Make sure object literals are declared consistently (defaults to explicit definitions). +- Use static Response methods instead of new Response() constructor when possible. +- Make sure switch-case statements are exhaustive. +- Make sure the `preconnect` attribute is used when using Google Fonts. +- Use `Array#{indexOf,lastIndexOf}()` instead of `Array#{findIndex,findLastIndex}()` when looking for the index of an item. +- Make sure iterable callbacks return consistent values. +- Use `with { type: "json" }` for JSON module imports. +- Use numeric separators in numeric literals. +- Use object spread instead of `Object.assign()` when constructing new objects. +- Always use the radix argument when using `parseInt()`. +- Make sure JSDoc comment lines start with a single asterisk, except for the first one. +- Include a description parameter for `Symbol()`. +- Don't use spread (`...`) syntax on accumulators. +- Don't use the `delete` operator. +- Don't access namespace imports dynamically. +- Don't use namespace imports. +- Declare regex literals at the top level. +- Don't use `target="_blank"` without `rel="noopener"`. + +### TypeScript Best Practices +- Don't use TypeScript enums. +- Don't export imported variables. +- Don't add type annotations to variables, parameters, and class properties that are initialized with literal expressions. +- Don't use TypeScript namespaces. +- Don't use non-null assertions with the `!` postfix operator. +- Don't use parameter properties in class constructors. +- Don't use user-defined types. +- Use `as const` instead of literal types and type annotations. +- Use either `T[]` or `Array` consistently. +- Initialize each enum member value explicitly. +- Use `export type` for types. +- Use `import type` for types. +- Make sure all enum members are literal values. +- Don't use TypeScript const enum. +- Don't declare empty interfaces. +- Don't let variables evolve into any type through reassignments. +- Don't use the any type. +- Don't misuse the non-null assertion operator (!) in TypeScript files. +- Don't use implicit any type on variable declarations. +- Don't merge interfaces and classes unsafely. +- Don't use overload signatures that aren't next to each other. +- Use the namespace keyword instead of the module keyword to declare TypeScript namespaces. + +### Style and Consistency +- Don't use global `eval()`. +- Don't use callbacks in asynchronous tests and hooks. +- Don't use negation in `if` statements that have `else` clauses. +- Don't use nested ternary expressions. +- Don't reassign function parameters. +- This rule lets you specify global variable names you don't want to use in your application. +- Don't use specified modules when loaded by import or require. +- Don't use constants whose value is the upper-case version of their name. +- Use `String.slice()` instead of `String.substr()` and `String.substring()`. +- Don't use template literals if you don't need interpolation or special-character handling. +- Don't use `else` blocks when the `if` block breaks early. +- Don't use yoda expressions. +- Don't use Array constructors. +- Use `at()` instead of integer index access. +- Follow curly brace conventions. +- Use `else if` instead of nested `if` statements in `else` clauses. +- Use single `if` statements instead of nested `if` clauses. +- Use `new` for all builtins except `String`, `Number`, and `Boolean`. +- Use consistent accessibility modifiers on class properties and methods. +- Use `const` declarations for variables that are only assigned once. +- Put default function parameters and optional function parameters last. +- Include a `default` clause in switch statements. +- Use the `**` operator instead of `Math.pow`. +- Use `for-of` loops when you need the index to extract an item from the iterated array. +- Use `node:assert/strict` over `node:assert`. +- Use the `node:` protocol for Node.js builtin modules. +- Use Number properties instead of global ones. +- Use assignment operator shorthand where possible. +- Use function types instead of object types with call signatures. +- Use template literals over string concatenation. +- Use `new` when throwing an error. +- Don't throw non-Error values. +- Use `String.trimStart()` and `String.trimEnd()` over `String.trimLeft()` and `String.trimRight()`. +- Use standard constants instead of approximated literals. +- Don't assign values in expressions. +- Don't use async functions as Promise executors. +- Don't reassign exceptions in catch clauses. +- Don't reassign class members. +- Don't compare against -0. +- Don't use labeled statements that aren't loops. +- Don't use void type outside of generic or return types. +- Don't use console. +- Don't use control characters and escape sequences that match control characters in regular expression literals. +- Don't use debugger. +- Don't assign directly to document.cookie. +- Use `===` and `!==`. +- Don't use duplicate case labels. +- Don't use duplicate class members. +- Don't use duplicate conditions in if-else-if chains. +- Don't use two keys with the same name inside objects. +- Don't use duplicate function parameter names. +- Don't have duplicate hooks in describe blocks. +- Don't use empty block statements and static blocks. +- Don't let switch clauses fall through. +- Don't reassign function declarations. +- Don't allow assignments to native objects and read-only global variables. +- Use Number.isFinite instead of global isFinite. +- Use Number.isNaN instead of global isNaN. +- Don't assign to imported bindings. +- Don't use irregular whitespace characters. +- Don't use labels that share a name with a variable. +- Don't use characters made with multiple code points in character class syntax. +- Make sure to use new and constructor properly. +- Don't use shorthand assign when the variable appears on both sides. +- Don't use octal escape sequences in string literals. +- Don't use Object.prototype builtins directly. +- Don't redeclare variables, functions, classes, and types in the same scope. +- Don't have redundant "use strict". +- Don't compare things where both sides are exactly the same. +- Don't let identifiers shadow restricted names. +- Don't use sparse arrays (arrays with holes). +- Don't use template literal placeholder syntax in regular strings. +- Don't use the then property. +- Don't use unsafe negation. +- Don't use var. +- Don't use with statements in non-strict contexts. +- Make sure async functions actually use await. +- Make sure default clauses in switch statements come last. +- Make sure to pass a message value when creating a built-in error. +- Make sure get methods always return a value. +- Use a recommended display strategy with Google Fonts. +- Make sure for-in loops include an if statement. +- Use Array.isArray() instead of instanceof Array. +- Make sure to use the digits argument with Number#toFixed(). +- Make sure to use the "use strict" directive in script files. + +### Next.js Specific Rules +- Don't use `` elements in Next.js projects. +- Don't use `` elements in Next.js projects. +- Don't import next/document outside of pages/_document.jsx in Next.js projects. +- Don't use the next/head module in pages/_document.js on Next.js projects. + +### Testing Best Practices +- Don't use export or module.exports in test files. +- Don't use focused tests. +- Make sure the assertion function, like expect, is placed inside an it() function call. +- Don't use disabled tests. + +## Common Tasks +- `npx ultracite init` - Initialize Ultracite in your project +- `npx ultracite format` - Format and fix code automatically +- `npx ultracite lint` - Check for issues without fixing + +## Example: Error Handling +```typescript +// ✅ Good: Comprehensive error handling +try { + const result = await fetchData(); + return { success: true, data: result }; +} catch (error) { + console.error('API call failed:', error); + return { success: false, error: error.message }; +} + +// ❌ Bad: Swallowing errors +try { + return await fetchData(); +} catch (e) { + console.log(e); +} +``` \ No newline at end of file diff --git a/packages/cli/template/nextjs-shadcn/.vscode/settings.json b/packages/cli/template/nextjs-shadcn/.vscode/settings.json new file mode 100644 index 00000000..1043bea0 --- /dev/null +++ b/packages/cli/template/nextjs-shadcn/.vscode/settings.json @@ -0,0 +1,35 @@ +{ + "editor.defaultFormatter": "esbenp.prettier-vscode", + "[javascript]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[typescript]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[javascriptreact]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[typescriptreact]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[json]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[jsonc]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[css]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[graphql]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "typescript.tsdk": "node_modules/typescript/lib", + "editor.formatOnSave": true, + "editor.formatOnPaste": true, + "emmet.showExpandedAbbreviation": "never", + "editor.codeActionsOnSave": { + "source.fixAll.biome": "explicit", + "source.organizeImports.biome": "explicit" + } +} \ No newline at end of file diff --git a/packages/cli/template/nextjs-shadcn/biome.json b/packages/cli/template/nextjs-shadcn/biome.json new file mode 100644 index 00000000..de591f68 --- /dev/null +++ b/packages/cli/template/nextjs-shadcn/biome.json @@ -0,0 +1,43 @@ +{ + "$schema": "https://biomejs.dev/schemas/2.2.0/schema.json", + "vcs": { + "enabled": true, + "clientKind": "git", + "useIgnoreFile": true + }, + "files": { + "ignoreUnknown": true, + "includes": [ + "**", + "!node_modules", + "!.next", + "!dist", + "!build" + ] + }, + "formatter": { + "enabled": true, + "indentStyle": "space", + "indentWidth": 2 + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true + }, + "domains": { + "next": "recommended", + "react": "recommended" + } + }, + "assist": { + "actions": { + "source": { + "organizeImports": "on" + } + } + }, + "extends": [ + "ultracite" + ] +} \ No newline at end of file diff --git a/packages/cli/template/nextjs-shadcn/components.json b/packages/cli/template/nextjs-shadcn/components.json index 7cccc384..ffe928f5 100644 --- a/packages/cli/template/nextjs-shadcn/components.json +++ b/packages/cli/template/nextjs-shadcn/components.json @@ -5,17 +5,17 @@ "tsx": true, "tailwind": { "config": "", - "css": "src/config/theme/globals.css", + "css": "src/app/globals.css", "baseColor": "neutral", "cssVariables": true, "prefix": "" }, "aliases": { "components": "@/components", - "utils": "@/utils/styles", + "utils": "@/lib/utils", "ui": "@/components/ui", - "lib": "@/utils", - "hooks": "@/utils/hooks" + "lib": "@/lib", + "hooks": "@/hooks" }, "iconLibrary": "lucide" -} +} \ No newline at end of file diff --git a/packages/cli/template/nextjs-shadcn/next.config.ts b/packages/cli/template/nextjs-shadcn/next.config.ts index 5a07c277..4e6fb1fe 100644 --- a/packages/cli/template/nextjs-shadcn/next.config.ts +++ b/packages/cli/template/nextjs-shadcn/next.config.ts @@ -1,8 +1,8 @@ -import { type NextConfig } from "next"; +import type { NextConfig } from 'next'; +import '@/lib/env'; -// Import env here to validate during build. -import "./src/config/env"; - -const nextConfig: NextConfig = {}; +const nextConfig: NextConfig = { + /* config options here */ +}; export default nextConfig; diff --git a/packages/cli/template/nextjs-shadcn/package.json b/packages/cli/template/nextjs-shadcn/package.json index 5c9833a7..0dfc82a2 100644 --- a/packages/cli/template/nextjs-shadcn/package.json +++ b/packages/cli/template/nextjs-shadcn/package.json @@ -1,42 +1,35 @@ { - "name": "template", + "name": "raw-next", "version": "0.1.0", "private": true, "scripts": { "dev": "next dev --turbopack", - "build": "next build", + "build": "next build --turbopack", "start": "next start", - "lint": "next lint", - "proofkit": "proofkit", - "typegen": "proofkit typegen", - "deploy": "proofkit deploy" + "lint": "biome check", + "format": "biome format --write" }, "dependencies": { - "@hookform/resolvers": "^5.1.1", - "@next-safe-action/adapter-react-hook-form": "^2.0.0", - "next-safe-action": "^8.0.4", - "react-hook-form": "^7.54.2", - "@tabler/icons-react": "^3.30.0", - "@t3-oss/env-nextjs": "^0.12.0", - "dayjs": "^1.11.13", - "next": "^15.2.3", - "react": "19.0.0", - "react-dom": "19.0.0", - "zod": "^3.24.2" + "@t3-oss/env-nextjs": "^0.13.8", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "lucide-react": "^0.541.0", + "next": "15.5.0", + "next-themes": "^0.4.6", + "react": "19.1.0", + "react-dom": "19.1.0", + "tailwind-merge": "^3.3.1" }, "devDependencies": { - "@types/node": "^20", - "@types/react": "npm:types-react@19.0.12", - "@types/react-dom": "npm:types-react-dom@19.0.4", - "eslint": "^9.14.0", - "eslint-config-next": "15.2.3", - "postcss": "^8.4.41", - "typescript": "^5" + "@biomejs/biome": "2.2.0", + "@tailwindcss/postcss": "^4", + "@types/node": "^22", + "@types/react": "^19", + "@types/react-dom": "^19", + "tailwindcss": "^4", + "tw-animate-css": "^1.3.7", + "typescript": "^5", + "ultracite": "5.2.4" }, - "pnpm": { - "overrides": { - "@types/react": "npm:types-react@19.0.0-rc.1", - "@types/react-dom": "npm:types-react-dom@19.0.0-rc.1" - } - } + "packageManager": "pnpm@10.14.0+sha512.ad27a79641b49c3e481a16a805baa71817a04bbe06a38d17e60e2eaee83f6a146c6a688125f5792e48dd5ba30e7da52a5cda4c3992b9ccf333f9ce223af84748" } diff --git a/packages/cli/template/nextjs-shadcn/postcss.config.cjs b/packages/cli/template/nextjs-shadcn/postcss.config.cjs deleted file mode 100644 index 483f3785..00000000 --- a/packages/cli/template/nextjs-shadcn/postcss.config.cjs +++ /dev/null @@ -1,5 +0,0 @@ -module.exports = { - plugins: { - "@tailwindcss/postcss": {}, - }, -}; diff --git a/packages/cli/template/nextjs-shadcn/postcss.config.mjs b/packages/cli/template/nextjs-shadcn/postcss.config.mjs new file mode 100644 index 00000000..c7bcb4b1 --- /dev/null +++ b/packages/cli/template/nextjs-shadcn/postcss.config.mjs @@ -0,0 +1,5 @@ +const config = { + plugins: ["@tailwindcss/postcss"], +}; + +export default config; diff --git a/packages/cli/template/nextjs-shadcn/src/app/(main)/page.tsx b/packages/cli/template/nextjs-shadcn/src/app/(main)/page.tsx index 5c9443b3..c6aafd11 100644 --- a/packages/cli/template/nextjs-shadcn/src/app/(main)/page.tsx +++ b/packages/cli/template/nextjs-shadcn/src/app/(main)/page.tsx @@ -1,46 +1,50 @@ -"use client"; +'use client'; import { - IconBrandGithub, - IconExternalLink, - IconCopy, - IconCheck, - IconTerminal, -} from "@tabler/icons-react"; -import { Button } from "@/components/ui/button"; -import { useState } from "react"; + CheckIcon, + CopyIcon, + ExternalLinkIcon, + GithubIcon, + TerminalIcon, +} from 'lucide-react'; +import { useState } from 'react'; +import { Button } from '@/components/ui/button'; function InlineSnippet({ command }: { command: string }) { const [copied, setCopied] = useState(false); const onCopy = () => { - if (typeof window === "undefined" || !navigator.clipboard?.writeText) + if (typeof window === 'undefined' || !navigator.clipboard?.writeText) { return; + } navigator.clipboard.writeText(command).then( () => { setCopied(true); - setTimeout(() => setCopied(false), 2000); + const timeoutInMilliseconds = 2000; + setTimeout(() => setCopied(false), timeoutInMilliseconds); }, - () => {}, + () => { + // do nothing + } ); }; return ( -
+
- +
- + {command}
@@ -52,61 +56,65 @@ export default function Home() {
+ {/** biome-ignore lint/performance/noImgElement: just a template image */} ProofKit -

Welcome!

+

Welcome!

-

+

This is the base template home page. To add more pages, components, or other features, run the ProofKit CLI from within your project.

-

+

To change this page, open src/app/(main)/page.tsx

-
+
- Sponsored by{" "} + Sponsored by{' '} Proof+Geist - {" "} - and{" "} + {' '} + and{' '} Ottomatic
diff --git a/packages/cli/template/nextjs-shadcn/src/config/theme/globals.css b/packages/cli/template/nextjs-shadcn/src/app/globals.css similarity index 72% rename from packages/cli/template/nextjs-shadcn/src/config/theme/globals.css rename to packages/cli/template/nextjs-shadcn/src/app/globals.css index 5a886a5a..dc98be74 100644 --- a/packages/cli/template/nextjs-shadcn/src/config/theme/globals.css +++ b/packages/cli/template/nextjs-shadcn/src/app/globals.css @@ -1,12 +1,46 @@ -/* Add global styles here */ - @import "tailwindcss"; @import "tw-animate-css"; -@theme { - --font-sans: - "Inter", "Geist", ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", - "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; +@custom-variant dark (&:is(.dark *)); + +@theme inline { + --color-background: var(--background); + --color-foreground: var(--foreground); + --font-sans: var(--font-geist-sans); + --font-mono: var(--font-geist-mono); + --color-sidebar-ring: var(--sidebar-ring); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar: var(--sidebar); + --color-chart-5: var(--chart-5); + --color-chart-4: var(--chart-4); + --color-chart-3: var(--chart-3); + --color-chart-2: var(--chart-2); + --color-chart-1: var(--chart-1); + --color-ring: var(--ring); + --color-input: var(--input); + --color-border: var(--border); + --color-destructive: var(--destructive); + --color-accent-foreground: var(--accent-foreground); + --color-accent: var(--accent); + --color-muted-foreground: var(--muted-foreground); + --color-muted: var(--muted); + --color-secondary-foreground: var(--secondary-foreground); + --color-secondary: var(--secondary); + --color-primary-foreground: var(--primary-foreground); + --color-primary: var(--primary); + --color-popover-foreground: var(--popover-foreground); + --color-popover: var(--popover); + --color-card-foreground: var(--card-foreground); + --color-card: var(--card); + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); } :root { @@ -47,73 +81,35 @@ .dark { --background: oklch(0.145 0 0); --foreground: oklch(0.985 0 0); - --card: oklch(0.145 0 0); + --card: oklch(0.205 0 0); --card-foreground: oklch(0.985 0 0); - --popover: oklch(0.145 0 0); + --popover: oklch(0.205 0 0); --popover-foreground: oklch(0.985 0 0); - --primary: oklch(0.85 0 0); + --primary: oklch(0.922 0 0); --primary-foreground: oklch(0.205 0 0); - --secondary: oklch(0.2 0 0); - --secondary-foreground: oklch(0.9 0 0); - --muted: oklch(0.2 0 0); - --muted-foreground: oklch(0.78 0 0); - --accent: oklch(0.22 0 0); - --accent-foreground: oklch(0.9 0 0); - --destructive: oklch(0.577 0.245 27.325); - --border: oklch(0.27 0 0); - --input: oklch(0.27 0 0); - --ring: oklch(0.52 0 0); - --chart-1: oklch(0.646 0.222 41.116); - --chart-2: oklch(0.6 0.118 184.704); - --chart-3: oklch(0.398 0.07 227.392); - --chart-4: oklch(0.828 0.189 84.429); - --chart-5: oklch(0.769 0.188 70.08); - --sidebar: oklch(0.18 0 0); - --sidebar-foreground: oklch(0.97 0 0); - --sidebar-primary: var(--primary); - --sidebar-primary-foreground: var(--primary-foreground); - --sidebar-accent: oklch(0.22 0 0); - --sidebar-accent-foreground: oklch(0.9 0 0); - --sidebar-border: oklch(0.27 0 0); - --sidebar-ring: oklch(0.52 0 0); -} - -@theme inline { - --radius-sm: calc(var(--radius) - 4px); - --radius-md: calc(var(--radius) - 2px); - --radius-lg: var(--radius); - --radius-xl: calc(var(--radius) + 4px); - --color-background: var(--background); - --color-foreground: var(--foreground); - --color-card: var(--card); - --color-card-foreground: var(--card-foreground); - --color-popover: var(--popover); - --color-popover-foreground: var(--popover-foreground); - --color-primary: var(--primary); - --color-primary-foreground: var(--primary-foreground); - --color-secondary: var(--secondary); - --color-secondary-foreground: var(--secondary-foreground); - --color-muted: var(--muted); - --color-muted-foreground: var(--muted-foreground); - --color-accent: var(--accent); - --color-accent-foreground: var(--accent-foreground); - --color-destructive: var(--destructive); - --color-border: var(--border); - --color-input: var(--input); - --color-ring: var(--ring); - --color-chart-1: var(--chart-1); - --color-chart-2: var(--chart-2); - --color-chart-3: var(--chart-3); - --color-chart-4: var(--chart-4); - --color-chart-5: var(--chart-5); - --color-sidebar: var(--sidebar); - --color-sidebar-foreground: var(--sidebar-foreground); - --color-sidebar-primary: var(--sidebar-primary); - --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); - --color-sidebar-accent: var(--sidebar-accent); - --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); - --color-sidebar-border: var(--sidebar-border); - --color-sidebar-ring: var(--sidebar-ring); + --secondary: oklch(0.269 0 0); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --accent: oklch(0.269 0 0); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.704 0.191 22.216); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: oklch(0.556 0 0); + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --sidebar: oklch(0.205 0 0); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.269 0 0); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(1 0 0 / 10%); + --sidebar-ring: oklch(0.556 0 0); } @layer base { diff --git a/packages/cli/template/nextjs-shadcn/src/app/layout.tsx b/packages/cli/template/nextjs-shadcn/src/app/layout.tsx index 70a51fc1..586db80d 100644 --- a/packages/cli/template/nextjs-shadcn/src/app/layout.tsx +++ b/packages/cli/template/nextjs-shadcn/src/app/layout.tsx @@ -1,33 +1,30 @@ -import { type Metadata } from "next"; -import "@/config/theme/globals.css"; - -export const metadata: Metadata = { - title: "My ProofKit App", - description: "Generated by proofkit", - icons: [{ rel: "icon", url: "/favicon.ico" }], -}; - -import { Geist, Geist_Mono } from "next/font/google"; - -import Providers from "@/components/providers"; +import type { Metadata } from 'next'; +import { Geist, Geist_Mono } from 'next/font/google'; +import './globals.css'; +import Providers from '@/components/providers'; const geistSans = Geist({ - variable: "--font-geist-sans", - subsets: ["latin"], + variable: '--font-geist-sans', + subsets: ['latin'], }); const geistMono = Geist_Mono({ - variable: "--font-geist-mono", - subsets: ["latin"], + variable: '--font-geist-mono', + subsets: ['latin'], }); +export const metadata: Metadata = { + title: 'My ProofKit App', + description: 'Generated by the ProofKit CLI', +}; + export default function RootLayout({ children, }: Readonly<{ children: React.ReactNode; }>) { return ( - + diff --git a/packages/cli/template/nextjs-shadcn/src/components/AppLogo.tsx b/packages/cli/template/nextjs-shadcn/src/components/AppLogo.tsx index f5ea4966..c1cd2554 100644 --- a/packages/cli/template/nextjs-shadcn/src/components/AppLogo.tsx +++ b/packages/cli/template/nextjs-shadcn/src/components/AppLogo.tsx @@ -1,6 +1,6 @@ -import { IconInfinity } from "@tabler/icons-react"; +import { InfinityIcon } from "lucide-react"; import React from "react"; export default function AppLogo() { - return ; -} + return ; +} \ No newline at end of file diff --git a/packages/cli/template/nextjs-shadcn/src/config/env.ts b/packages/cli/template/nextjs-shadcn/src/lib/env.ts similarity index 78% rename from packages/cli/template/nextjs-shadcn/src/config/env.ts rename to packages/cli/template/nextjs-shadcn/src/lib/env.ts index 3c50ef8d..2139d54c 100644 --- a/packages/cli/template/nextjs-shadcn/src/config/env.ts +++ b/packages/cli/template/nextjs-shadcn/src/lib/env.ts @@ -8,6 +8,5 @@ export const env = createEnv({ .default("development"), }, client: {}, - // For Next.js >= 13.4.4, you only need to destructure client variables: experimental__runtimeEnv: {}, }); diff --git a/packages/cli/template/nextjs-shadcn/src/lib/utils.ts b/packages/cli/template/nextjs-shadcn/src/lib/utils.ts new file mode 100644 index 00000000..bd0c391d --- /dev/null +++ b/packages/cli/template/nextjs-shadcn/src/lib/utils.ts @@ -0,0 +1,6 @@ +import { clsx, type ClassValue } from "clsx" +import { twMerge } from "tailwind-merge" + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} diff --git a/packages/cli/template/nextjs-shadcn/src/server/safe-action.ts b/packages/cli/template/nextjs-shadcn/src/server/safe-action.ts deleted file mode 100644 index 7f62198a..00000000 --- a/packages/cli/template/nextjs-shadcn/src/server/safe-action.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { createSafeActionClient } from "next-safe-action"; - -export const actionClient = createSafeActionClient(); diff --git a/packages/cli/template/nextjs-shadcn/src/utils/notification-helpers.ts b/packages/cli/template/nextjs-shadcn/src/utils/notification-helpers.ts deleted file mode 100644 index 1a8d7ebb..00000000 --- a/packages/cli/template/nextjs-shadcn/src/utils/notification-helpers.ts +++ /dev/null @@ -1,16 +0,0 @@ -export function showErrorNotification(): void; -export function showErrorNotification(message: string): void; -export function showErrorNotification(args?: string): void { - const message = - typeof args === "string" ? args : "An unexpected error occurred."; - // TODO: Replace with your preferred toast library - if (typeof window !== "undefined") console.error(message); -} - -export function showSuccessNotification(): void; -export function showSuccessNotification(message: string): void; -export function showSuccessNotification(args?: string): void { - const message = typeof args === "string" ? args : "Success!"; - // TODO: Replace with your preferred toast library - if (typeof window !== "undefined") console.log(message); -} diff --git a/packages/cli/template/nextjs-shadcn/src/utils/styles.ts b/packages/cli/template/nextjs-shadcn/src/utils/styles.ts deleted file mode 100644 index 1f4cd38e..00000000 --- a/packages/cli/template/nextjs-shadcn/src/utils/styles.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { clsx } from "clsx"; -import { twMerge } from "tailwind-merge"; - -export function cn(...inputs: any[]) { - return twMerge(clsx(inputs)); -} diff --git a/packages/cli/template/nextjs-shadcn/tsconfig.json b/packages/cli/template/nextjs-shadcn/tsconfig.json index 51d0dbce..dd41d9d9 100644 --- a/packages/cli/template/nextjs-shadcn/tsconfig.json +++ b/packages/cli/template/nextjs-shadcn/tsconfig.json @@ -1,6 +1,11 @@ { "compilerOptions": { - "lib": ["dom", "dom.iterable", "esnext"], + "target": "ES2017", + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], "allowJs": true, "skipLibCheck": true, "strict": true, @@ -18,10 +23,19 @@ } ], "paths": { - "@/*": ["./src/*"] + "@/*": [ + "./src/*" + ] }, - "target": "ES2017" + "strictNullChecks": true }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], - "exclude": ["node_modules"] -} + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts" + ], + "exclude": [ + "node_modules" + ] +} \ No newline at end of file From a72a9a2b2c7ccaed835727e17bca7d0c9608df55 Mon Sep 17 00:00:00 2001 From: Eric Luce <37158449+eluce2@users.noreply.github.com> Date: Sun, 24 Aug 2025 21:08:44 -0500 Subject: [PATCH 4/6] Refactor CLI and registry templates to support schema selection and enhance handlebars processing. Introduce new post-install steps for templates, update metadata structure, and improve error handling in template installation. Add support for dynamic route names in static components and ensure compatibility with Shadcn. Update dependencies and template paths for better consistency. --- .cursor/plans/static-registry.md | 2 +- apps/docs/src/app/r/[[...name]]/registry.ts | 19 +- packages/cli/src/cli/add/registry/install.ts | 160 +++++++++++-- .../add/registry/postInstall/handlebars.ts | 25 +- .../src/cli/add/registry/postInstall/index.ts | 2 + .../add/registry/postInstall/wrap-provider.ts | 18 +- packages/cli/src/helpers/createProject.ts | 4 + .../src/installers/dependencyVersionMap.ts | 3 + .../cli/template/nextjs-shadcn/proofkit.json | 5 +- .../components/AppShell/internal/AppShell.tsx | 2 +- packages/registry/lib/types.ts | 53 ++--- packages/registry/lib/utils.ts | 100 +++++++- packages/registry/lib/validator.ts | 2 +- .../registry/templates/better-auth/_meta.ts | 16 +- .../registry/templates/better-auth/auth.hbs | 103 +++------ .../templates/components/mode-toggle/_meta.ts | 1 - .../templates/email/auth-code/_meta.ts | 1 - .../registry/templates/email/generic/_meta.ts | 1 - .../registry/templates/react-email/_meta.ts | 1 - .../registry/templates/table/basic/README.hbs | 215 ++++++++++++++++++ .../registry/templates/table/basic/_meta.ts | 36 +++ .../templates/table/basic/constants.ts | 1 + .../registry/templates/table/basic/page.hbs | 88 +++++++ .../registry/templates/table/basic/table.hbs | 162 +++++++++++++ .../registry/templates/utils/nuqs/_meta.ts | 27 +++ .../registry/templates/utils/t3-env/_meta.ts | 37 +-- .../registry/templates/utils/t3-env/env.ts | 14 +- test_handlebars.js | 23 ++ 28 files changed, 931 insertions(+), 190 deletions(-) create mode 100644 packages/registry/templates/table/basic/README.hbs create mode 100644 packages/registry/templates/table/basic/_meta.ts create mode 100644 packages/registry/templates/table/basic/constants.ts create mode 100644 packages/registry/templates/table/basic/page.hbs create mode 100644 packages/registry/templates/table/basic/table.hbs create mode 100644 packages/registry/templates/utils/nuqs/_meta.ts create mode 100644 test_handlebars.js diff --git a/.cursor/plans/static-registry.md b/.cursor/plans/static-registry.md index 196f091b..3c26a355 100644 --- a/.cursor/plans/static-registry.md +++ b/.cursor/plans/static-registry.md @@ -112,7 +112,7 @@ app.get("/index.json", async (c) => { // Serves the data for a single component // The :style param is part of the shadcn spec, we'll include it for compatibility app.get("/:style/:name.json", async (c) => { - const { name } = c.req.param(); + const { name } = c.req.param(); try { const component = await getStaticComponent(name); if (!component) { diff --git a/apps/docs/src/app/r/[[...name]]/registry.ts b/apps/docs/src/app/r/[[...name]]/registry.ts index 6d9c4555..a155ab50 100644 --- a/apps/docs/src/app/r/[[...name]]/registry.ts +++ b/apps/docs/src/app/r/[[...name]]/registry.ts @@ -4,6 +4,7 @@ import { getComponentMeta, getRegistryIndex, getStaticComponent, + getStaticComponentForShadcn, } from "@proofkit/registry"; import { createMiddleware } from "hono/factory"; import type { TemplateMetadata } from "@proofkit/registry"; @@ -53,10 +54,14 @@ app.get("/*", componentMeta("/r"), async (c) => { const path = c.get("path"); const requestUrl = new URL(c.req.url); - const meta = c.get("meta"); - if (meta.type === "static") { - try { - const data = await getStaticComponent(path); + const routeNameRaw = c.req.query("routeName") + ? decodeURIComponent(c.req.query("routeName")!) + : undefined; + // remove leading slash if present + const routeName = routeNameRaw ? routeNameRaw.replace(/^\/+/, "") : undefined; + + try { + const data = await getStaticComponentForShadcn(path, {routeName}); return c.json({ ...data, @@ -68,12 +73,6 @@ app.get("/*", componentMeta("/r"), async (c) => { console.error(error); return c.json({ error: "Component not found." }, { status: 404 }); } - } else { - return c.json( - { error: "Dynamic components are not supported yet." }, - { status: 501 }, - ); - } }); export default app; diff --git a/packages/cli/src/cli/add/registry/install.ts b/packages/cli/src/cli/add/registry/install.ts index 8c6340a3..be56bb04 100644 --- a/packages/cli/src/cli/add/registry/install.ts +++ b/packages/cli/src/cli/add/registry/install.ts @@ -1,12 +1,17 @@ import { getOtherProofKitDependencies } from "@proofkit/registry"; -import { uniq } from "es-toolkit"; +import { uniq, capitalize } from "es-toolkit"; import ora from "ora"; import semver from "semver"; +import * as p from "@clack/prompts"; import { getRegistryUrl, shadcnInstall } from "~/helpers/shadcn-cli.js"; import { getVersion } from "~/utils/getProofKitVersion.js"; import { logger } from "~/utils/logger.js"; -import { getSettings, mergeSettings } from "~/utils/parseSettings.js"; +import { getSettings, mergeSettings, type DataSource } from "~/utils/parseSettings.js"; +import { getExistingSchemas } from "~/generators/fmdapi.js"; +import { addRouteToNav } from "~/generators/route.js"; +import { state } from "~/state.js"; +import { abortIfCancel } from "~/cli/utils.js"; import { getMetaFromRegistry } from "./getOptions.js"; import { buildHandlebarsData, @@ -16,6 +21,40 @@ import { import { processPostInstallStep } from "./postInstall/index.js"; import { preflightAddCommand } from "./preflight.js"; +async function promptForSchemaFromDataSource({ + projectDir = process.cwd(), + dataSource, +}: { + projectDir?: string; + dataSource: DataSource; +}) { + if (dataSource.type === "supabase") { + throw new Error("Not implemented"); + } else { + const schemas = getExistingSchemas({ + projectDir, + dataSourceName: dataSource.name, + }) + .map((s) => s.schemaName) + .filter(Boolean); + + if (schemas.length === 0) { + p.cancel("This data source doesn't have any schemas to load data from"); + return undefined; + } + + if (schemas.length === 1) return schemas[0]; + + const schemaName = abortIfCancel( + await p.select({ + message: "Which schema should this template use?", + options: schemas.map((o) => ({ label: o, value: o ?? "" })), + }) + ); + return schemaName; + } +} + export async function installFromRegistry(name: string) { const spinner = ora("Validating template").start(); await preflightAddCommand(); @@ -40,31 +79,124 @@ export async function installFromRegistry(name: string) { spinner.succeed(); const otherProofKitDependencies = getOtherProofKitDependencies(meta); - const previouslyInstalledTemplates = getSettings().registryTemplates; + // Handle schema requirement if template needs it + let dataSource: DataSource | undefined; + let schemaName: string | undefined; + let routeName: string | undefined; + let pageName: string | undefined; + + if (meta.category === "page") { + // Prompt user for the URL path of the page + routeName = abortIfCancel( + await p.text({ + message: `Enter the URL PATH for your new page`, + placeholder: "/my-page", + validate: (value) => { + if (value.length === 0) { + return "URL path is required"; + } + return; + }, + }) + ); + + if (routeName.startsWith("/")) { + routeName = routeName.slice(1); + } + + pageName = capitalize(routeName.replace("/", "").trim()); + } + + if (meta.schemaRequired) { + const settings = getSettings(); + + if (settings.dataSources.length === 0) { + spinner.fail("This template requires a data source, but you don't have any. Add a data source first."); + return; + } + + const dataSourceName = settings.dataSources.length > 1 + ? abortIfCancel( + await p.select({ + message: "Which data source should be used for this template?", + options: settings.dataSources.map((ds) => ({ + value: ds.name, + label: ds.name, + })), + }) + ) + : settings.dataSources[0]?.name; + + dataSource = settings.dataSources.find( + (ds) => ds.name === dataSourceName + ); + + if (!dataSource) { + spinner.fail(`Data source ${dataSourceName} not found`); + return; + } + + schemaName = await promptForSchemaFromDataSource({ + projectDir: state.projectDir, + dataSource, + }); + + if (!schemaName) { + spinner.fail("Schema selection was cancelled"); + return; + } + } + // if dynamic, figure out what fields to pass, then construct the URL to send to shadcn. Otherwise, just send the URL to shadcn - let url = `${getRegistryUrl()}/r/${name}`; - if (meta.type === "dynamic") { - throw new Error("Dynamic templates are not yet supported"); - } else if (meta.type === "static") { - // just send the URL to shadcn - } else { - throw new Error("Unknown template type"); + let url = new URL(`${getRegistryUrl()}/r/${name}`); + if (meta.category === "page") { + url.searchParams.set("routeName", `/(main)/${routeName??name}`); } - // run shadcn command - await shadcnInstall([url], meta.title); + await shadcnInstall([url.toString()], meta.title); const handlebarsFiles = meta.files.filter((file) => file.handlebars); if (handlebarsFiles.length > 0) { - const templateData = buildHandlebarsData(); - for (const file of handlebarsFiles) { + // Build template data with schema information if available + const templateData = dataSource && schemaName + ? buildHandlebarsData({ + dataSource, + schemaName, + }) + : buildHandlebarsData(); + + // Add page information to template data if available + if (routeName) { + (templateData as any).routeName = routeName; + } + if (pageName) { + (templateData as any).pageName = pageName; + } + + // Resolve __PATH__ placeholders in file paths before handlebars processing + const resolvedFiles = handlebarsFiles.map(file => ({ + ...file, + destinationPath: file.destinationPath?.replace('__PATH__', `/(main)/${routeName??name}`) + })); + + for (const file of resolvedFiles) { await randerHandlebarsToFile(file, templateData); } } + // Add route to navigation if this is a page template + if (meta.category === "page" && routeName && pageName) { + await addRouteToNav({ + projectDir: state.projectDir, + navType: "primary", + label: pageName, + href: `/${routeName}`, + }); + } + // if post-install steps, process those if (meta.postInstall) { for (const step of meta.postInstall) { diff --git a/packages/cli/src/cli/add/registry/postInstall/handlebars.ts b/packages/cli/src/cli/add/registry/postInstall/handlebars.ts index 26b6d41b..fad7d75d 100644 --- a/packages/cli/src/cli/add/registry/postInstall/handlebars.ts +++ b/packages/cli/src/cli/add/registry/postInstall/handlebars.ts @@ -1,5 +1,5 @@ import path from "path"; -import { TemplateFile } from "@proofkit/registry"; +import { TemplateFile, decodeHandlebarsFromShadcn } from "@proofkit/registry"; import fs from "fs-extra"; import handlebars from "handlebars"; @@ -83,7 +83,13 @@ export function buildHandlebarsData(args?: DataSourceForTemplate) { return { proofkit, shadcn, - schema: args ? buildDataSourceData(args) : undefined, + schema: args ? buildDataSourceData(args) : { + sourceName: "UnknownDataSource", + schemaName: "UnknownSchema", + clientSuffix: "UnknownClientSuffix", + allFieldNames: ["UnknownFieldName"], + fieldNames: ["UnknownFieldName"], + }, }; } @@ -92,9 +98,13 @@ export async function randerHandlebarsToFile( data: ReturnType ) { const inputPath = getFilePath(file, data); - const rawTemplate = await fs.readFile(inputPath, "utf8"); + let rawTemplate = await fs.readFile(inputPath, "utf8"); + + // Decode placeholder tokens back to handlebars syntax + // This uses the centralized decoding function from the registry package + rawTemplate = decodeHandlebarsFromShadcn(rawTemplate); + const template = handlebars.compile(rawTemplate); - const rendered = template(data); await fs.writeFile(inputPath, rendered); } @@ -153,6 +163,13 @@ export function getFilePath( case "registry:file": return path.join(cwd, "src", thePath); case "registry:page": + // For page templates, use the route name if available in template data + const routeName = (data as any).routeName; + if (routeName) { + // Add /(main) prefix for Next.js app router structure + const pageRoute = routeName === "/" ? "" : routeName; + return path.join(cwd, "src", "app", "(main)", pageRoute, thePath); + } return path.join(cwd, "src", "app", thePath); case "registry:block": return path.join(cwd, "src", "components", "blocks", thePath); diff --git a/packages/cli/src/cli/add/registry/postInstall/index.ts b/packages/cli/src/cli/add/registry/postInstall/index.ts index 99e4bfdc..bd2b8c0d 100644 --- a/packages/cli/src/cli/add/registry/postInstall/index.ts +++ b/packages/cli/src/cli/add/registry/postInstall/index.ts @@ -9,6 +9,8 @@ export async function processPostInstallStep(step: PostInstallStep) { addScriptToPackageJson(step); } else if (step.action === "wrap provider") { await wrapProvider(step); + } else if (step.action === "next-steps") { + logger.info(step.data.message); } else { logger.error(`Unknown post-install step: ${step}`); } diff --git a/packages/cli/src/cli/add/registry/postInstall/wrap-provider.ts b/packages/cli/src/cli/add/registry/postInstall/wrap-provider.ts index 82f09b92..512db6f2 100644 --- a/packages/cli/src/cli/add/registry/postInstall/wrap-provider.ts +++ b/packages/cli/src/cli/add/registry/postInstall/wrap-provider.ts @@ -11,9 +11,7 @@ import { state } from "~/state.js"; import { logger } from "~/utils/logger.js"; import { formatAndSaveSourceFiles, getNewProject } from "~/utils/ts-morph.js"; -export async function wrapProvider( - step: Extract -) { +export async function wrapProvider(step: Extract) { const { parentTag, imports: importConfigs, @@ -115,9 +113,21 @@ export async function wrapProvider( // If no parent tag found or specified, wrap the entire return statement const returnExpression = returnStatement?.getExpression(); if (returnExpression) { + // Check if the expression is a ParenthesizedExpression + const isParenthesized = returnExpression.getKind() === SyntaxKind.ParenthesizedExpression; + + let innerExpressionText: string; + if (isParenthesized) { + // Get the inner expression from the parenthesized expression + const parenthesizedExpr = returnExpression.asKindOrThrow(SyntaxKind.ParenthesizedExpression); + innerExpressionText = parenthesizedExpr.getExpression().getText(); + } else { + innerExpressionText = returnExpression.getText(); + } + const newReturnContent = `return ( ${providerOpenTag} - ${returnExpression.getText()} + ${innerExpressionText} ${providerCloseTag} );`; diff --git a/packages/cli/src/helpers/createProject.ts b/packages/cli/src/helpers/createProject.ts index 7f57dae1..3904c0f3 100644 --- a/packages/cli/src/helpers/createProject.ts +++ b/packages/cli/src/helpers/createProject.ts @@ -90,6 +90,10 @@ export const createBareProject = async ({ dependencies: SHADCN_BASE_DEV_DEPS, devMode: true, }); + addPackageDependency({ + dependencies: ["zod"], + devMode: false, + }); } else { throw new Error(`Unsupported UI library: ${state.ui}`); } diff --git a/packages/cli/src/installers/dependencyVersionMap.ts b/packages/cli/src/installers/dependencyVersionMap.ts index 3811390c..71b961f3 100644 --- a/packages/cli/src/installers/dependencyVersionMap.ts +++ b/packages/cli/src/installers/dependencyVersionMap.ts @@ -106,5 +106,8 @@ export const dependencyVersionMap = { // Theme utilities "next-themes": "^0.4.6", + + // Zod + "zod": "^4", } as const; export type AvailableDependencies = keyof typeof dependencyVersionMap; diff --git a/packages/cli/template/nextjs-shadcn/proofkit.json b/packages/cli/template/nextjs-shadcn/proofkit.json index 6aca1027..13d3916d 100644 --- a/packages/cli/template/nextjs-shadcn/proofkit.json +++ b/packages/cli/template/nextjs-shadcn/proofkit.json @@ -1,7 +1,6 @@ { - "auth": { "type": "none" }, + "ui": "shadcn", "envFile": ".env", "appType": "browser", - "ui": "shadcn", - "appliedUpgrades": ["shadcn", "cursorRules"] + "registryTemplates": ["utils/t3-env"] } diff --git a/packages/cli/template/nextjs-shadcn/src/components/AppShell/internal/AppShell.tsx b/packages/cli/template/nextjs-shadcn/src/components/AppShell/internal/AppShell.tsx index 33ccb2df..d842e03c 100644 --- a/packages/cli/template/nextjs-shadcn/src/components/AppShell/internal/AppShell.tsx +++ b/packages/cli/template/nextjs-shadcn/src/components/AppShell/internal/AppShell.tsx @@ -15,7 +15,7 @@ export default function MainAppShell({
-
+
{children}
diff --git a/packages/registry/lib/types.ts b/packages/registry/lib/types.ts index 2af89daa..f90824c0 100644 --- a/packages/registry/lib/types.ts +++ b/packages/registry/lib/types.ts @@ -41,19 +41,27 @@ export const templateFileSchema = z.discriminatedUnion("type", [ }), ]); -export const postInstallStepsSchema = z.discriminatedUnion("action", [ - z.object({ - action: z.literal("package.json script"), +const buildPostInstallStepsSchema = ( + action: A, + dataSchema: T +) => { + return z.object({ + action: z.literal(action), + data: dataSchema, _from: z.string().optional(), - data: z.object({ + }); +}; + +export const postInstallStepsSchema = z.discriminatedUnion("action", [ + buildPostInstallStepsSchema("next-steps" , z.object({ + message: z.string(), + })), + buildPostInstallStepsSchema("package.json script", z.object({ scriptName: z.string(), scriptCommand: z.string(), }), - }), - z.object({ - action: z.literal("wrap provider"), - _from: z.string().optional(), - data: z.object({ + ), + buildPostInstallStepsSchema("wrap provider" , z.object({ providerOpenTag: z .string() .describe( @@ -91,8 +99,8 @@ export const postInstallStepsSchema = z.discriminatedUnion("action", [ .describe( "If set, the provider will attempt to go inside of the parent tag. The first found tag will be used as the parent. If not set or none of the tags are found, the provider will be wrapped at the very top level.", ), - }), - }), + }), + ), ]); export type PostInstallStep = z.infer; @@ -107,8 +115,9 @@ const categorySchema = z.enum([ export const frameworkSchema = z.enum(["next-pages", "next-app", "manual"]); -const sharedMetadataSchema = registryItemSchema - .omit({ name: true, type: true, files: true }) +// Defines the metadata for a single template (_meta.ts) +export const templateMetadataSchema = registryItemSchema + .omit({ name: true, type: true, files: true, docs: true }) .extend({ title: z.string(), description: z.string().optional(), @@ -126,25 +135,17 @@ const sharedMetadataSchema = registryItemSchema .describe("The minimum version of ProofKit required to use this template") .optional(), allowedFrameworks: z.array(frameworkSchema).optional(), + schemaRequired: z + .boolean() + .optional() + .describe("Whether this template requires a database schema to be selected"), }); -// Defines the metadata for a single template (_meta.ts) -export const templateMetadataSchema = z.discriminatedUnion("type", [ - sharedMetadataSchema.extend({ - type: z.literal("static"), - }), - sharedMetadataSchema.extend({ - type: z.literal("dynamic"), - schema: z.unknown(), // a JSON schema for the required values to be passed as query(?) params - }), -]); - export type TemplateFile = z.infer; export type TemplateMetadata = z.infer; -export const registryIndexSchema = sharedMetadataSchema +export const registryIndexSchema = templateMetadataSchema .pick({ title: true, category: true, description: true }) - .extend({ type: z.enum(["static", "dynamic"]) }) .array(); // Adapt shadcn RegistryItem: require `content` in files and allow both single and array forms diff --git a/packages/registry/lib/utils.ts b/packages/registry/lib/utils.ts index 454bfdf8..abd38634 100644 --- a/packages/registry/lib/utils.ts +++ b/packages/registry/lib/utils.ts @@ -16,7 +16,6 @@ const templatesPath = path.resolve(__dirname, "../templates"); export type RegistryIndexItem = { name: string; - type: TemplateMetadata["type"]; category: TemplateMetadata["category"]; // files: string[]; // destination paths }; @@ -162,40 +161,39 @@ export async function getComponentMeta( export async function getStaticComponent( namePath: string, + options?: { routeName?: string }, ): Promise { const normalized = getNormalizedPath(namePath); const meta = await getComponentMeta(namePath); - if (meta.type !== "static") { - throw new Error(`Template "${normalized}" is not a static template`); - } - const files: ShadcnFilesUnion = await Promise.all( meta.files.map(async (file) => { const sourceFile = file.handlebars ? file.sourceFileName.replace(/\.[^/.]+$/, ".hbs") : file.sourceFileName; - const contentPath = path.join( - templatesPath, - normalized, - sourceFile, - ); + const contentPath = path.join(templatesPath, normalized, sourceFile); const content = await fs.readFile(contentPath, "utf-8"); + const routeName = options?.routeName ?? namePath; + const destinationPath = file.destinationPath?.replace( + "__PATH__", + routeName, + ); + const shadcnFile: ShadcnFilesUnion[number] = file.type === "registry:file" || file.type === "registry:page" ? { path: file.sourceFileName, type: file.type, content, - target: file.destinationPath ?? file.sourceFileName, + target: destinationPath ?? file.sourceFileName, } : { path: file.sourceFileName, type: file.type, content, - target: file.destinationPath! + target: destinationPath!, }; return shadcnFile; @@ -209,3 +207,81 @@ export async function getStaticComponent( files, }; } + +/** + * Template transformation utilities for handling handlebars syntax in shadcn CLI + */ + +/** + * Mapping of handlebars expressions to TypeScript-safe placeholder tokens + */ +const HANDLEBARS_PLACEHOLDERS = { + '{{schema.schemaName}}': '__HB_SCHEMA_NAME__', + '{{schema.sourceName}}': '__HB_SOURCE_NAME__', + '{{schema.clientSuffix}}': '__HB_CLIENT_SUFFIX__', + // Add more mappings as needed +} as const; + +/** + * Converts handlebars expressions in template content to TypeScript-safe placeholder tokens + * This allows the shadcn CLI to process the templates without TypeScript parsing errors + */ +export function encodeHandlebarsForShadcn(content: string): string { + let result = content; + + // Replace specific handlebars expressions with placeholder tokens + for (const [handlebars, placeholder] of Object.entries(HANDLEBARS_PLACEHOLDERS)) { + result = result.replace(new RegExp(escapeRegExp(handlebars), 'g'), placeholder); + } + + return result; +} + +/** + * Converts placeholder tokens back to handlebars expressions + * This is used by the CLI after shadcn has processed the templates + */ +export function decodeHandlebarsFromShadcn(content: string): string { + let result = content; + + // Replace placeholder tokens back to handlebars expressions + for (const [handlebars, placeholder] of Object.entries(HANDLEBARS_PLACEHOLDERS)) { + result = result.replace(new RegExp(escapeRegExp(placeholder), 'g'), handlebars); + } + + return result; +} + +/** + * Escapes special regex characters in a string + */ +function escapeRegExp(string: string): string { + return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} + +/** + * Enhanced version of getStaticComponent that applies handlebars encoding for shadcn compatibility + */ +export async function getStaticComponentForShadcn( + namePath: string, + options?: { routeName?: string }, +): Promise { + const component = await getStaticComponent(namePath, options); + + // Apply handlebars encoding to files that need it + const encodedFiles = component.files.map(file => { + // Only encode handlebars files that might contain problematic expressions + if (file.path.endsWith('.hbs') || file.path.endsWith('.tsx') || file.path.endsWith('.ts')) { + return { + ...file, + content: encodeHandlebarsForShadcn(file.content), + }; + } + return file; + }); + + return { + ...component, + files: encodedFiles, + }; +} diff --git a/packages/registry/lib/validator.ts b/packages/registry/lib/validator.ts index 6420f461..a15716e4 100644 --- a/packages/registry/lib/validator.ts +++ b/packages/registry/lib/validator.ts @@ -2,7 +2,7 @@ import fs from "fs"; import path from "path"; import { fileURLToPath } from "url"; import createJiti from "jiti"; -import { templateMetadataSchema, type TemplateMetadata } from "./types.js"; +import { templateMetadataSchema } from "./types"; export interface ValidationContext { templatesPath: string; diff --git a/packages/registry/templates/better-auth/_meta.ts b/packages/registry/templates/better-auth/_meta.ts index 7f1c7f14..bea9ac32 100644 --- a/packages/registry/templates/better-auth/_meta.ts +++ b/packages/registry/templates/better-auth/_meta.ts @@ -1,7 +1,6 @@ import type { TemplateMetadata } from "@/lib/types"; export const meta: TemplateMetadata = { - type: "static", title: "BetterAuth", description: "A better auth library for Next.js", category: "utility", @@ -16,7 +15,7 @@ export const meta: TemplateMetadata = { "{proofkit}/r/react-email", "{proofkit}/r/email/generic", ], - css: { '@source "../../../node_modules/@daveyplate/better-auth-ui"': {} }, + css: { '@source "../../node_modules/@daveyplate/better-auth-ui"': {} }, files: [ { sourceFileName: "main-layout.tsx", @@ -70,7 +69,7 @@ export const meta: TemplateMetadata = { namedImports: ["AuthUIProvider"], }, { - moduleSpecifier: "@/auth-client", + moduleSpecifier: "@/lib/auth-client", namedImports: ["authClient"], }, ], @@ -81,14 +80,3 @@ export const meta: TemplateMetadata = { }, ], }; - -/** - * add to css - * @source "../../../node_modules/@daveyplate/better-auth-ui"; - * - * package.json script - * "better-auth:migrate": "pnpm dlx @proofkit/better-auth@latest migrate" - * - * Wrap the app in AuthUIProvider - * - */ diff --git a/packages/registry/templates/better-auth/auth.hbs b/packages/registry/templates/better-auth/auth.hbs index 0dbe3e8e..aae898e0 100644 --- a/packages/registry/templates/better-auth/auth.hbs +++ b/packages/registry/templates/better-auth/auth.hbs @@ -1,71 +1,32 @@ -import { FileMakerAdapter } from "@proofkit/better-auth"; -import { betterAuth } from "better-auth"; -import { nextCookies } from "better-auth/next-js"; -import { env } from "@/registry/default/lib/env"; -import { render } from "@react-email/components"; -import { GenericEmail } from "@/emails/generic"; - -export const auth = betterAuth({ - // database - database: FileMakerAdapter({ - debugLogs: true, - {{#findFirst proofkit.dataSources "fm"}} - odata: { - serverUrl: env.{{envNames.server}}, - auth: { apiKey: env.{{envNames.apiKey}} }, - database: env.{{envNames.database}}, - }, - {{else}} - // TODO: Add your FileMaker data source here - // odata: { - // serverUrl: env.FM_SERVER, - // auth: { apiKey: env.OTTO_API_KEY }, - // database: env.FM_DATABASE, - // }, - {{/findFirst}} - }), - - emailAndPassword: { - enabled: true, - autoSignIn: true, // Automatically sign in the user after sign up - sendResetPassword: async ({ url, user }) => { - // this is the HTML body of the email to be send - const body = await render( - GenericEmail({ - title: "Reset Your Password", - description: "Click the link to reset your password", - ctaText: "Reset Password", - ctaHref: url, - footer: - "If you did not request a password reset, please ignore this email.", - }), - ); - const subject = "Reset Your Password"; - - // TODO: Customize this function to actually send the email to your users - // Learn more: https://proofkit.dev/auth/better-auth - console.warn( - "TODO: Customize this function to actually send to your users", - ); - console.log( - `To ${user.email}: Click the link to reset your password: ${url}`, - ); - }, - - // if set to true, you'll have to create users in FileMaker first - disableSignUp: false, - }, - - // --- CUSTOMIZE TABLE NAMES HERE --- - // Account table: Stores OAuth tokens and/or hashed passwords - account: { modelName: "account" }, - // User table: Stores user data - user: { modelName: "user" }, - // Session table: The record that determines if a user is logged in - session: { modelName: "session" }, - // Verification table: Stores email verification and password reset tokens - verification: { modelName: "verification" }, - - // --- PLUGINS --- - plugins: [nextCookies()], -}); +import { FileMakerAdapter } from "@proofkit/better-auth"; import { betterAuth } +from "better-auth"; import { nextCookies } from "better-auth/next-js"; import { +env } from "@/registry/default/lib/env"; import { render } from +"@react-email/components"; import { GenericEmail } from "@/emails/generic"; +export const auth = betterAuth({ // database database: FileMakerAdapter({ +debugLogs: true, +{{#findFirst proofkit.dataSources "fm"}} + odata: { serverUrl: env.{{envNames.server}}, auth: { apiKey: env.{{envNames.apiKey}} + }, database: env.{{envNames.database}}, }, +{{else}} + // TODO: Add your FileMaker data source here // odata: { // serverUrl: + env.FM_SERVER, // auth: { apiKey: env.OTTO_API_KEY }, // database: + env.FM_DATABASE, // }, +{{/findFirst}} +}), emailAndPassword: { enabled: true, autoSignIn: true, // Automatically sign +in the user after sign up sendResetPassword: async ({ url, user }) => { // this +is the HTML body of the email to be send const body = await render( +GenericEmail({ title: "Reset Your Password", description: "Click the link to +reset your password", ctaText: "Reset Password", ctaHref: url, footer: "If you +did not request a password reset, please ignore this email.", }), ); const +subject = "Reset Your Password"; // TODO: Customize this function to actually +send the email to your users // Learn more: +https://proofkit.dev/auth/better-auth console.warn( "TODO: Customize this +function to actually send to your users", ); console.log( `To ${user.email}: +Click the link to reset your password: ${url}`, ); }, // if set to true, you'll +have to create users in FileMaker first disableSignUp: false, }, // --- +CUSTOMIZE TABLE NAMES HERE --- // Account table: Stores OAuth tokens and/or +hashed passwords account: { modelName: "account" }, // User table: Stores user +data user: { modelName: "user" }, // Session table: The record that determines +if a user is logged in session: { modelName: "session" }, // Verification table: +Stores email verification and password reset tokens verification: { modelName: +"verification" }, // --- PLUGINS --- plugins: [nextCookies()], }); \ No newline at end of file diff --git a/packages/registry/templates/components/mode-toggle/_meta.ts b/packages/registry/templates/components/mode-toggle/_meta.ts index f7c1fbdd..714e2bac 100644 --- a/packages/registry/templates/components/mode-toggle/_meta.ts +++ b/packages/registry/templates/components/mode-toggle/_meta.ts @@ -1,7 +1,6 @@ import type { TemplateMetadata } from "@/lib/types"; export const meta: TemplateMetadata = { - type: "static", title: "Mode Toggle", description: "A toggle button to switch between light and dark mode.", diff --git a/packages/registry/templates/email/auth-code/_meta.ts b/packages/registry/templates/email/auth-code/_meta.ts index 3a94e6ff..6cd0428e 100644 --- a/packages/registry/templates/email/auth-code/_meta.ts +++ b/packages/registry/templates/email/auth-code/_meta.ts @@ -1,7 +1,6 @@ import type { TemplateMetadata } from "@/lib/types"; export const meta: TemplateMetadata = { - type: "static", title: "Auth Code Email", description: diff --git a/packages/registry/templates/email/generic/_meta.ts b/packages/registry/templates/email/generic/_meta.ts index a3d443a7..145accb2 100644 --- a/packages/registry/templates/email/generic/_meta.ts +++ b/packages/registry/templates/email/generic/_meta.ts @@ -1,7 +1,6 @@ import type { TemplateMetadata } from "@/lib/types"; export const meta: TemplateMetadata = { - type: "static", title: "Generic Email Template", description: diff --git a/packages/registry/templates/react-email/_meta.ts b/packages/registry/templates/react-email/_meta.ts index 1e0589d1..5498211b 100644 --- a/packages/registry/templates/react-email/_meta.ts +++ b/packages/registry/templates/react-email/_meta.ts @@ -1,7 +1,6 @@ import type { TemplateMetadata } from "@/lib/types"; export const meta: TemplateMetadata = { - type: "static", title: "React Email", description: "Build and send emails using React and TypeScript.", author: "https://react.email/docs", diff --git a/packages/registry/templates/table/basic/README.hbs b/packages/registry/templates/table/basic/README.hbs new file mode 100644 index 00000000..f09260de --- /dev/null +++ b/packages/registry/templates/table/basic/README.hbs @@ -0,0 +1,215 @@ +# Data Table Template + +A comprehensive, production-ready data table implementation demonstrating modern React and Next.js patterns. + +## 📋 Overview + +This template shows how to build a data table with: +- Server-side pagination and sorting +- URL-synchronized state management +- TypeScript integration +- FileMaker database connectivity +- Accessibility features + +## 🏗️ Architecture + +### File Structure +``` +table/ # this folder name defines the URL path to this page +├── README.md # This documentation +├── constants.ts # Configuration constants +├── page.tsx # Server Component (data fetching) +└── table.tsx # Client Component (user interactions) +``` + +### Component Responsibilities + +**`page.tsx` - Server Component** +- Runs on the server before the page is sent to the browser +- Parses URL search parameters +- Fetches data from FileMaker database +- Handles SEO and initial page rendering +- Cannot use browser features (onClick, useState, etc.) + +**`table.tsx` - Client Component** +- Runs in the browser after page loads +- Handles user interactions (sorting, pagination) +- Manages UI state and loading states +- Cannot directly access databases or server resources + +## 🔄 Data Flow + +1. **User Navigation**: User visits `/table?page=2&sortBy=nameFirst` +2. **Server Processing**: `page.tsx` parses URL params and queries database +3. **Data Fetching**: FileMaker API returns paginated, sorted data +4. **Server Rendering**: Page is rendered on server with data included +5. **Client Hydration**: `table.tsx` becomes interactive in browser +6. **User Interaction**: User clicks sort → URL updates → Server re-renders + +## 🎯 Key Concepts for New Developers + +### 1. Server vs Client Components + +**Server Components** (page.tsx): +- ✅ Direct database access +- ✅ Environment variables +- ✅ SEO optimization +- ✅ Fast initial rendering +- ❌ No user interactions +- ❌ No React hooks (useState, useEffect) + +**Client Components** (table.tsx): +- ✅ User interactions +- ✅ React hooks +- ✅ Browser APIs +- ✅ Real-time updates +- ❌ No direct database access +- ❌ No server-only resources + +### 2. URL State Management + +Instead of storing table state in React state, we use URL parameters: + +``` +/table?page=2&pageSize=20&sortBy=nameFirst&sortOrder=desc +``` + +**Benefits:** +- Bookmarkable links +- Shareable URLs +- Browser back/forward works +- State persists on refresh + +**Implementation:** +- Server: `nuqs/server` for parsing parameters +- Client: `nuqs` hooks for updating parameters + +### 3. Performance Optimizations + +**`useMemo`**: Prevents expensive recalculations +```typescript +const columns = useMemo(() => [...], []); // Only create once +``` + +**`useTransition`**: Shows loading states during navigation +```typescript +const [isPending, startTransition] = useTransition(); +startTransition(() => { + // State updates that trigger navigation +}); +``` + +**Server-side Processing**: Pagination and sorting happen on the server, reducing data transfer. + +### 4. TypeScript Integration + +**Type Safety**: Database schema types ensure correct field usage +```typescript +type TData = T{{schema.schemaName}}; // Generated from FileMaker schema +``` + +## 🛠️ Configuration + +### Pagination Settings + +Edit `constants.ts` to change default page size: +```typescript +export const DEFAULT_PAGE_SIZE = 10; // Adjust as needed +``` + +### Adding Columns + +1. **Update Type Definition**: Ensure field exists in `T{{schema.schemaName}}` type +2. **Add Column Definition** in `table.tsx`: +```typescript +{ + id: 'newField', + header: ({ column }) => ( + + ), + accessorFn: (row) => row.newField, + enableSorting: true, +} +``` + +### Database Integration + +The template uses FileMaker Data API through `@proofkit/fmdapi`: + +```typescript +const { data, dataInfo } = await {{schema.schemaName}}{{schema.clientSuffix}}.list({ + limit: pageSize, // Items per page + offset, // Skip records for pagination + sort, // Sorting configuration +}); +``` + +**Important**: Default limit is 100 records. For more, see ProofKit documentation. + +## 🔍 Troubleshooting + +### Data Loading Issues +1. **Run type generation first**: `pnpm typegen` +2. Check FileMaker schema changes +3. Verify environment variables (without exposing values) + +### Common Patterns + +**Adding Loading States:** +```typescript +{isPending &&
Loading...
} +``` + +**Error Handling:** +```typescript +try { + const data = await fetchData(); +} catch (error) { + console.error('Failed to load data:', error); +} +``` + +## 🚀 Extending the Template + +### Adding Filtering +1. Add filter parameters to `tableSearchParams` +2. Update server query logic +3. Add filter UI components + +### Custom Sorting +1. Modify sort parameter building in `page.tsx` +2. Update FileMaker query configuration +3. Add custom sort options to column headers + +### Export Functionality +1. Create server action for data export +2. Add export button to table toolbar +3. Handle different export formats (CSV, Excel, etc.) + +## 📚 Learning Resources + +- [Next.js App Router](https://nextjs.org/docs/app) +- [TanStack Table](https://tanstack.com/table/latest) +- [nuqs Documentation](https://nuqs.47ng.com/) +- [ProofKit FileMaker Integration](https://proofkit.dev/docs) + +## 🎨 Styling + +This template uses: +- **Tailwind CSS**: Utility-first CSS framework +- **shadcn/ui**: Pre-built accessible components +- **CSS Modules**: For component-specific styles + +Customize appearance by modifying: +- Tailwind classes in JSX +- Component variants in `ui/` components +- Global styles in `globals.css` + +## 🔐 Security Notes + +- Database credentials stay on the server +- Client never sees sensitive environment variables +- All database queries are server-side only +- Input validation happens at multiple layers + +--- \ No newline at end of file diff --git a/packages/registry/templates/table/basic/_meta.ts b/packages/registry/templates/table/basic/_meta.ts new file mode 100644 index 00000000..5927a4eb --- /dev/null +++ b/packages/registry/templates/table/basic/_meta.ts @@ -0,0 +1,36 @@ +import { TemplateMetadata } from "@/lib/types"; + +export const meta: TemplateMetadata = { + title: "Basic Table", + description: "A basic table with data fetched from the database", + category: "page", + registryType: "registry:page", + dependencies: [], + registryDependencies: ["{proofkit}/r/utils/nuqs","https://reui.io/r/data-grid.json","https://reui.io/r/scroll-area.json"], + schemaRequired: true, + files: [ + { + sourceFileName: "page.tsx", + destinationPath: "src/app/__PATH__/page.tsx", + type: "registry:page", + handlebars: true, + }, + { + sourceFileName: "table.tsx", + destinationPath: "src/app/__PATH__/table.tsx", + type: "registry:page", + handlebars: true, + }, + { + sourceFileName: "constants.ts", + destinationPath: "src/app/__PATH__/constants.ts", + type: "registry:page", + }, + { + sourceFileName: "README.md", + destinationPath: "src/app/__PATH__/README.md", + type: "registry:page", + handlebars: true, + }, + ], +}; diff --git a/packages/registry/templates/table/basic/constants.ts b/packages/registry/templates/table/basic/constants.ts new file mode 100644 index 00000000..459723e8 --- /dev/null +++ b/packages/registry/templates/table/basic/constants.ts @@ -0,0 +1 @@ +export const DEFAULT_PAGE_SIZE = 10; diff --git a/packages/registry/templates/table/basic/page.hbs b/packages/registry/templates/table/basic/page.hbs new file mode 100644 index 00000000..dd2ffcc4 --- /dev/null +++ b/packages/registry/templates/table/basic/page.hbs @@ -0,0 +1,88 @@ +import { + createLoader, + parseAsInteger, + parseAsString, + type SearchParams, +} from 'nuqs/server'; +import type { T{{schema.schemaName}} } from '@/config/schemas/{{schema.sourceName}}/{{schema.schemaName}}'; +import { {{schema.schemaName}}{{schema.clientSuffix}} } from '@/config/schemas/{{schema.sourceName}}/client'; +import { DEFAULT_PAGE_SIZE } from './constants'; +import TableContent from './table'; + +// URL search parameter configuration +export const tableSearchParams = { + page: parseAsInteger.withDefault(1), + pageSize: parseAsInteger.withDefault(DEFAULT_PAGE_SIZE), + sortBy: parseAsString, + sortOrder: parseAsString, +}; + +const loadSearchParams = createLoader(tableSearchParams); + +type PageProps = { + searchParams: Promise; +}; + +export default async function TablePage({ searchParams }: PageProps) { + const { page, pageSize, sortBy, sortOrder } = + await loadSearchParams(searchParams); + + const offset = (page - 1) * pageSize; + + // Build sort parameter for FileMaker API + const sort = + sortBy && sortOrder + ? [ + { + fieldName: sortBy as keyof T{{schema.schemaName}}, + sortOrder: sortOrder === 'desc' ? 'descend' : 'ascend', + }, + ] + : undefined; + + // Fetch data from FileMaker database + // Limited to 100 records by default - see docs for more + const { data, dataInfo } = await {{schema.schemaName}}{{schema.clientSuffix}}.list({ + limit: pageSize, + offset, + ...(sort ? { sort } : {}), + // fetch: { next: { revalidate: 60 } }, // Optional caching + }); + + return ( +
+ d.fieldData)} + totalCount={dataInfo.totalRecordCount} + /> +

+ + Data Grid + {' '} + component by reui and{' '} + + Tanstack Table + + . Learn more or get more components at{' '} + + reui.io + +

+
+ ); +} diff --git a/packages/registry/templates/table/basic/table.hbs b/packages/registry/templates/table/basic/table.hbs new file mode 100644 index 00000000..ed85652f --- /dev/null +++ b/packages/registry/templates/table/basic/table.hbs @@ -0,0 +1,162 @@ +'use client'; + +import { + type ColumnDef, + getCoreRowModel, + type SortingState, + useReactTable, +} from '@tanstack/react-table'; + +import { parseAsInteger, parseAsString, useQueryState } from 'nuqs'; +import { useMemo, useTransition } from 'react'; +import { DataGrid, DataGridContainer } from '@/components/ui/data-grid'; +import { DataGridColumnHeader } from '@/components/ui/data-grid-column-header'; +import { DataGridPagination } from '@/components/ui/data-grid-pagination'; +import { DataGridTable } from '@/components/ui/data-grid-table'; +import { ScrollArea, ScrollBar } from '@/components/ui/scroll-area'; +import { Skeleton } from '@/components/ui/skeleton'; +import type { T{{schema.schemaName}} } from '@/config/schemas/{{schema.sourceName}}/{{schema.schemaName}}'; +import { DEFAULT_PAGE_SIZE } from './constants'; + +type TData = T{{schema.schemaName}}; + +export default function MyTable({ + data, + totalCount, +}: { + data: TData[]; + totalCount: number | undefined; +}) { + const [isPending, startTransition] = useTransition(); + + // URL-synchronized state for pagination and sorting + const [pageIndex, setPagination] = useQueryState( + 'page', + parseAsInteger.withDefault(1).withOptions({ shallow: false }) + ); + const [pageSize, setPageSize] = useQueryState( + 'pageSize', + parseAsInteger + .withDefault(DEFAULT_PAGE_SIZE) + .withOptions({ shallow: false }) + ); + const [sortBy, setSortBy] = useQueryState( + 'sortBy', + parseAsString.withOptions({ shallow: false }) + ); + const [sortOrder, setSortOrder] = useQueryState( + 'sortOrder', + parseAsString.withOptions({ shallow: false }) + ); + + // Convert URL params to React Table sorting format + const sorting: SortingState = useMemo(() => { + if (sortBy && sortOrder) { + return [{ id: sortBy, desc: sortOrder === 'desc' }]; + } + return []; + }, [sortBy, sortOrder]); + + const handleSortingChange = ( + updaterOrValue: SortingState | ((old: SortingState) => SortingState) + ) => { + const newSorting = + typeof updaterOrValue === 'function' + ? updaterOrValue(sorting) + : updaterOrValue; + + startTransition(() => { + if (newSorting.length === 0) { + setSortBy(null); + setSortOrder(null); + } else { + const sort = newSorting[0]; + setSortBy(sort.id); + setSortOrder(sort.desc ? 'desc' : 'asc'); + } + }); + }; + // Column definitions - memoized for performance + const columns = useMemo[]>( + () => + [ +{{#each schema.fieldNames}} + { + id: '{{this}}', + header: ({ column }) => ( + + ), + accessorFn: (row) => row["{{this}}"], + enableSorting: true, + meta: { + skeleton: , + }, + }, +{{/each}} + ] satisfies ColumnDef[], + [] + ); + // Configure React Table instance + const table = useReactTable({ + columns, + data, + pageCount: Math.ceil((totalCount || 0) / pageSize), + getRowId: (row: TData) => row.id, + state: { + pagination: { pageIndex: pageIndex - 1, pageSize }, + sorting, + }, + onPaginationChange: (pagination) => { + startTransition(() => { + if (typeof pagination === 'function') { + const newPagination = pagination({ + pageIndex: pageIndex - 1, + pageSize, + }); + + // Reset to page 1 if page size changed + if (newPagination.pageSize !== pageSize) { + setPagination(1); + setPageSize(newPagination.pageSize); + } else { + setPagination(newPagination.pageIndex + 1); + setPageSize(newPagination.pageSize); + } + return; + } + + if (pagination.pageSize !== pageSize) { + setPagination(1); + setPageSize(pagination.pageSize); + } else { + setPagination(pagination.pageIndex + 1); + setPageSize(pagination.pageSize); + } + }); + }, + onSortingChange: handleSortingChange, + getCoreRowModel: getCoreRowModel(), + // Server-side processing + manualPagination: true, + manualSorting: true, + manualFiltering: true, + }); + return ( + +
+ + + + + + + +
+
+ ); +} diff --git a/packages/registry/templates/utils/nuqs/_meta.ts b/packages/registry/templates/utils/nuqs/_meta.ts new file mode 100644 index 00000000..2ea877c2 --- /dev/null +++ b/packages/registry/templates/utils/nuqs/_meta.ts @@ -0,0 +1,27 @@ +import { TemplateMetadata } from "@/lib/types"; + +export const meta: TemplateMetadata = { + title: "Nuqs", + description: "A utility for managing query parameters in the URL", + category: "utility", + registryType: "registry:internal", + dependencies: ["nuqs"], + registryDependencies: [], + files: [], + postInstall: [ + { + action: "wrap provider", + data: { + providerOpenTag: "", + providerCloseTag: "", + imports: [ + // import { NuqsAdapter } from 'nuqs/adapters/next/app'; + { + moduleSpecifier: "nuqs/adapters/next/app", + namedImports: ["NuqsAdapter"], + }, + ], + }, + }, + ], +}; diff --git a/packages/registry/templates/utils/t3-env/_meta.ts b/packages/registry/templates/utils/t3-env/_meta.ts index 6698569e..d29f0f70 100644 --- a/packages/registry/templates/utils/t3-env/_meta.ts +++ b/packages/registry/templates/utils/t3-env/_meta.ts @@ -1,18 +1,23 @@ import { TemplateMetadata } from "@/lib/types"; -export const meta: TemplateMetadata={ - category:"utility", - title:"T3 Env Validation", - description:"A utility to validate environment variables", - registryType:"registry:lib", - type:"static", - dependencies:["@t3-oss/env-nextjs", "zod"], - docs:"Be sure to import the env.ts file into your next.config.ts to validate at build time.", - files:[ - { - type:"registry:lib", - sourceFileName:"env.ts", - - } - ] -} \ No newline at end of file +export const meta: TemplateMetadata = { + category: "utility", + title: "T3 Env Validation", + description: "A utility to validate environment variables", + registryType: "registry:lib", + dependencies: ["@t3-oss/env-nextjs", "zod"], + files: [ + { + type: "registry:lib", + sourceFileName: "env.ts", + }, + ], + postInstall: [ + { + action: "next-steps", + data: { + message: "Be sure to import the env.ts file into your next.config.ts to validate at build time.", + }, + }, + ], +}; diff --git a/packages/registry/templates/utils/t3-env/env.ts b/packages/registry/templates/utils/t3-env/env.ts index 2f88a1b4..2a9149ea 100644 --- a/packages/registry/templates/utils/t3-env/env.ts +++ b/packages/registry/templates/utils/t3-env/env.ts @@ -1,14 +1,10 @@ import { createEnv } from "@t3-oss/env-nextjs"; import { z } from "zod"; - + export const env = createEnv({ server: { - NODE_ENV: z.enum(["development", "production"]), - }, - client: { - + NODE_ENV: z.enum(["development", "production"]), }, - experimental__runtimeEnv:{ - - } -}); \ No newline at end of file + client: {}, + experimental__runtimeEnv: {}, +}); diff --git a/test_handlebars.js b/test_handlebars.js new file mode 100644 index 00000000..cd45afb0 --- /dev/null +++ b/test_handlebars.js @@ -0,0 +1,23 @@ +// Simulate the complete workflow +const originalTemplate = 'import type { T__hbo__schema.schemaName__hbc__ } from \'@/config/schemas/__hbo__schema.sourceName__hbc__/__hbo__schema.schemaName__hbc__\';'; + +console.log('1. Original template (what shadcn CLI sees):'); +console.log(originalTemplate); +console.log(''); + +// Step 1: shadcn CLI processes this - should be valid TypeScript +const isValidForShadcn = !originalTemplate.includes('{{') && !originalTemplate.includes('}}'); +console.log('2. Valid for shadcn CLI?', isValidForShadcn); +console.log(''); + +// Step 2: Our handlebars processor decodes the tokens +const decodedTemplate = originalTemplate.replace(/__hbo__/g, '{{').replace(/__hbc__/g, '}}'); +console.log('3. After handlebars decoding:'); +console.log(decodedTemplate); +console.log(''); + +// Step 3: Handlebars processes with actual data +const mockData = { schema: { schemaName: 'User', sourceName: 'mydb' } }; +const finalResult = decodedTemplate.replace(/\{\{schema\.schemaName\}\}/g, mockData.schema.schemaName).replace(/\{\{schema\.sourceName\}\}/g, mockData.schema.sourceName); +console.log('4. Final rendered result:'); +console.log(finalResult); From f1d6331f90ef4c9561af7bcf78fba5e061c8900e Mon Sep 17 00:00:00 2001 From: Eric Luce <37158449+eluce2@users.noreply.github.com> Date: Sun, 24 Aug 2025 21:11:56 -0500 Subject: [PATCH 5/6] update changeset --- .changeset/ui-default-shadcn.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.changeset/ui-default-shadcn.md b/.changeset/ui-default-shadcn.md index 6a54d283..3260d4fa 100644 --- a/.changeset/ui-default-shadcn.md +++ b/.changeset/ui-default-shadcn.md @@ -2,4 +2,6 @@ "@proofkit/cli": minor --- -CLI defaults to shadcn/ui for new projects. Legacy Mantine templates are still available via a hidden `--ui mantine` flag during `init`. The selected UI is persisted in `proofkit.json` as `ui`. Existing projects using Mantine are auto-detected and remain fully supported. For shadcn-based projects, adding new pages or auth via `proofkit add` is temporarily disabled while we work on a new component system. \ No newline at end of file +CLI defaults to shadcn/ui for new projects. Legacy Mantine templates are still available via a hidden `--ui mantine` flag during `init`. The selected UI is persisted in `proofkit.json` as `ui`. Existing projects using Mantine are auto-detected and remain fully supported. For shadcn-based projects, adding new pages or auth via `proofkit add` requires passing the name of the component you want to add, such as `proofkit add table/basic`. Interactive selection of templates will come back soon! + +This release also deprecates the `mantine` UI templates. In the next major release of the CLI, the `mantine` UI templates will no longer be supported for new projects. \ No newline at end of file From b7c2f729c0296b209e0a9f3ead6ba6f3008e2246 Mon Sep 17 00:00:00 2001 From: Eric Luce <37158449+eluce2@users.noreply.github.com> Date: Sun, 24 Aug 2025 21:14:29 -0500 Subject: [PATCH 6/6] update to beta --- .changeset/pre.json | 21 +++++++++++++++++++++ packages/cli/package.json | 2 +- packages/fmdapi/CHANGELOG.md | 6 ++++++ packages/fmdapi/package.json | 2 +- packages/typegen/CHANGELOG.md | 7 +++++++ packages/typegen/package.json | 2 +- 6 files changed, 37 insertions(+), 3 deletions(-) create mode 100644 .changeset/pre.json diff --git a/.changeset/pre.json b/.changeset/pre.json new file mode 100644 index 00000000..7bdcd4c2 --- /dev/null +++ b/.changeset/pre.json @@ -0,0 +1,21 @@ +{ + "mode": "pre", + "tag": "beta", + "initialVersions": { + "demo": "0.1.0", + "@proofkit/docs": "0.0.2", + "@proofkit/better-auth": "0.3.0", + "@proofkit/cli": "1.1.8", + "create-proofkit": "0.1.0", + "@proofkit/fmdapi": "5.0.2", + "fmodata": "0.0.0", + "@proofkit/registry": "1.0.0", + "tmp": "0.0.0", + "@proofkit/typegen": "1.0.10", + "@proofkit/webviewer": "3.0.6" + }, + "changesets": [ + "swift-swans-rush", + "ui-default-shadcn" + ] +} diff --git a/packages/cli/package.json b/packages/cli/package.json index 55f2358e..2e18fe15 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@proofkit/cli", - "version": "1.1.8", + "version": "1.2.0-beta.0", "description": "Create web application with the ProofKit stack", "license": "MIT", "repository": { diff --git a/packages/fmdapi/CHANGELOG.md b/packages/fmdapi/CHANGELOG.md index dcbe55b2..8e6b5f1e 100644 --- a/packages/fmdapi/CHANGELOG.md +++ b/packages/fmdapi/CHANGELOG.md @@ -1,5 +1,11 @@ # @proofkit/fmdapi +## 5.0.3-beta.0 + +### Patch Changes + +- 78cbab1: Update removeFMTableNames function for proper type-safety + ## 5.0.2 ### Patch Changes diff --git a/packages/fmdapi/package.json b/packages/fmdapi/package.json index 4594f4be..c1a9d95e 100644 --- a/packages/fmdapi/package.json +++ b/packages/fmdapi/package.json @@ -1,6 +1,6 @@ { "name": "@proofkit/fmdapi", - "version": "5.0.2", + "version": "5.0.3-beta.0", "description": "FileMaker Data API client", "repository": "git@github.com:proofgeist/fm-dapi.git", "author": "Eric <37158449+eluce2@users.noreply.github.com>", diff --git a/packages/typegen/CHANGELOG.md b/packages/typegen/CHANGELOG.md index 7b60c085..cb0c50a1 100644 --- a/packages/typegen/CHANGELOG.md +++ b/packages/typegen/CHANGELOG.md @@ -1,5 +1,12 @@ # @proofkit/typegen +## 1.0.11-beta.0 + +### Patch Changes + +- Updated dependencies [78cbab1] + - @proofkit/fmdapi@5.0.3-beta.0 + ## 1.0.10 ### Patch Changes diff --git a/packages/typegen/package.json b/packages/typegen/package.json index 564d1547..370df30e 100644 --- a/packages/typegen/package.json +++ b/packages/typegen/package.json @@ -1,6 +1,6 @@ { "name": "@proofkit/typegen", - "version": "1.0.10", + "version": "1.0.11-beta.0", "description": "", "type": "module", "main": "dist/esm/index.js",