Skip to content
Open
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
1d3b74b
fix(create-nextly-app): validate --template against the template regi…
aqib-rx Jul 23, 2026
83fb16a
fix(create-nextly-app): skip app-shaped project detection for plugin …
aqib-rx Jul 23, 2026
9993cf4
fix(create-nextly-app): materialize dev/.env when scaffolding a plugin
aqib-rx Jul 23, 2026
1bf5c59
fix(create-nextly-app): wrap the plugin playground admin in QueryProv…
aqib-rx Jul 23, 2026
4a8587c
fix(create-nextly-app): update the plugin template test to the curren…
aqib-rx Jul 23, 2026
94a866a
feat(create-nextly-app): seed the dev auto-login user in the plugin p…
aqib-rx Jul 23, 2026
35585e0
fix(create-nextly-app): scaffold plugins from the bundled template
aqib-rx Jul 23, 2026
e376185
chore(release): add changeset for the plugin template fixes
aqib-rx Jul 23, 2026
4b3c8a0
fix(create-nextly-app): keep the latest dist-tag out of plugin peer d…
aqib-rx Jul 23, 2026
9e7f5dc
fix(create-nextly-app): close the super-admin seeding race in the plu…
aqib-rx Jul 23, 2026
b4c8313
fix(create-nextly-app): honor a non-main --branch for bundled templates
aqib-rx Jul 23, 2026
4913601
fix(create-nextly-app): restrict playground credential seeding to nex…
aqib-rx Jul 23, 2026
a042580
docs(create-nextly-app): explain the direct api usage in the plugin t…
aqib-rx Jul 23, 2026
ed4ccb6
fix(create-nextly-app): yalc-link plugin-sdk for plugin scaffolds
aqib-rx Jul 23, 2026
532c100
chore(release): add admin-css to the plugin template changeset
aqib-rx Jul 23, 2026
5869983
Merge remote-tracking branch 'origin/main' into fix/plugin-template-s…
muzzamil-rx Jul 27, 2026
fb9b7bb
Merge remote-tracking branch 'origin/main' into fix/plugin-template-s…
aqib-rx Jul 28, 2026
ebc7d2c
chore(release): add blocks-engine and plugin-seo to the plugin templa…
aqib-rx Jul 28, 2026
1cd8634
fix(create-nextly-app): harden plugin scaffolds against packing and c…
aqib-rx Jul 28, 2026
fe1320f
fix(create-nextly-app): pin yalc plugin scaffolds' nested resolutions…
aqib-rx Jul 28, 2026
ef7c789
fix(create-nextly-app): scaffold plugins from inside an existing next…
aqib-rx Jul 28, 2026
a369327
Merge remote-tracking branch 'origin/main' into fix/plugin-template-s…
aqib-rx Jul 29, 2026
0ec3970
fix(create-nextly-app): merge restored gitignores and validate regist…
aqib-rx Jul 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions .changeset/plugin-template-scaffold-fixes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
---
"@nextlyhq/adapter-drizzle": patch
"@nextlyhq/adapter-mysql": patch
"@nextlyhq/adapter-postgres": patch
"@nextlyhq/adapter-sqlite": patch
"@nextlyhq/admin": patch
"@nextlyhq/admin-css": patch
"create-nextly-app": patch
"@nextlyhq/eslint-config": patch
"nextly": patch
"@nextlyhq/plugin-form-builder": patch
"@nextlyhq/plugin-page-builder": patch
"@nextlyhq/plugin-sdk": patch
"@nextlyhq/prettier-config": patch
"@nextlyhq/storage-s3": patch
"@nextlyhq/storage-uploadthing": patch
"@nextlyhq/storage-vercel-blob": patch
"@nextlyhq/telemetry": patch
"@nextlyhq/tsconfig": patch
"@nextlyhq/ui": patch
Comment thread
aqib-rx marked this conversation as resolved.
Comment thread
aqib-rx marked this conversation as resolved.
---

The Plugin template now works end to end. `--template plugin` is accepted, the
scaffold no longer aborts with "App Router not detected", the dev playground's
env file is created automatically, `/admin` renders (QueryProvider) and lands
already logged in as the seeded dev user, the generated test suite matches the
current plugin-sdk API, and plugin scaffolds use the template bundled with the
CLI so it can no longer drift ahead of the published packages.
56 changes: 56 additions & 0 deletions packages/create-nextly-app/src/__tests__/scaffold-plugin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,17 @@ describe("scaffold --template plugin (D44/D45 smoke test)", () => {
expect(pkg.files).toEqual(["dist"]);
expect(pkg.keywords).toContain("nextly-plugin");
expect(pkg.scripts.dev).toContain("next dev dev");

// peerDependencies must never carry the "latest" dist-tag: pnpm 11
// rejects it (ERR_PNPM_INVALID_PEER_DEPENDENCY_SPECIFICATION) and then
// refuses to run any script in the scaffold. This test runs with the
// registry stubbed offline, so every version lookup exercises the
// fallback path — exactly where the dist-tag used to leak in.
for (const [peer, spec] of Object.entries(
pkg.peerDependencies as Record<string, string>
)) {
expect(spec, `peerDependencies.${peer}`).not.toBe("latest");
}
// The native-build allowlist lives in pnpm-workspace.yaml, NOT the package.json
// `pnpm` field (pnpm 11 ignores that field). Without this, `pnpm install` aborts
// on better-sqlite3 (the dev playground's native dep) with ERR_PNPM_IGNORED_BUILDS.
Expand All @@ -89,6 +100,51 @@ describe("scaffold --template plugin (D44/D45 smoke test)", () => {
expect(workspaceYaml).toContain("allowBuilds:");
expect(workspaceYaml).toContain("better-sqlite3");

// The dev playground must boot with zero manual steps: without dev/.env
// the dialect defaults to postgresql and `next dev` aborts in the
// instrumentation hook asking for DATABASE_URL. The scaffold
// materializes the committed example env into the real one.
expect(await exists(path.join(target, "dev/.env"))).toBe(true);
const devEnv = await readFile(path.join(target, "dev/.env"), "utf-8");
expect(devEnv).toContain("DB_DIALECT=sqlite");

// /admin must render through QueryProvider — the admin's data hooks
// resolve their QueryClient from it, and mounting RootLayout without it
// crashes the page on first load ("No QueryClient set").
const adminPage = await readFile(
path.join(target, "dev/src/app/admin/[[...params]]/page.tsx"),
"utf-8"
);
expect(adminPage).toContain("QueryProvider");
expect(adminPage).toContain("ErrorBoundary");

// The generated test must match the current harness + Direct API: the
// harness applies plugin schema contributions itself (passing them again
// via `collections` is a slug collision), and CRUD methods take a single
// args object (`create({ collection, data })`, `findByID({ ... })`).
const pluginTest = await readFile(
path.join(target, "src/plugin.test.ts"),
"utf-8"
);
expect(pluginTest).not.toContain("contributes?.collections");
expect(pluginTest).toContain("findByID({");
expect(pluginTest).toContain("create({");

// The playground seeds the auto-login user at boot; without it the first
// /admin visit dead-ends on the setup wizard despite devAutoLogin. The
// seed must run twice: the runtime's background permission seeding races
// the first pass, and the second pass (role-exists path) tops up any
// permissions created in between so the role is complete either way.
const instrumentation = await readFile(
path.join(target, "dev/instrumentation.ts"),
"utf-8"
);
expect(instrumentation).toContain("seedSuperAdmin");
expect(instrumentation.match(/await seedDevUser\(\)/g)).toHaveLength(2);
// Credential seeding must be locked to `next dev` — a broader guard
// (e.g. !== "production") would also seed under NODE_ENV=test.
expect(instrumentation).toContain('process.env.NODE_ENV === "development"');

// Placeholders are replaced everywhere (no leftover {{ ... }} tokens).
const pluginSrc = await readFile(
path.join(target, "src/plugin.ts"),
Expand Down
31 changes: 31 additions & 0 deletions packages/create-nextly-app/src/__tests__/templates.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
import {
getAvailableTemplateNames,
getTemplate,
shouldUseBundledTemplate,
templateHasApproaches,
templateHasDemoData,
} from "../lib/templates";
Expand Down Expand Up @@ -80,3 +81,33 @@ describe("plugin package.json generation (D44/D45)", () => {
expect(pkg.dependencies).toBeUndefined();
});
});

describe("shouldUseBundledTemplate", () => {
it("uses the bundled copy for blank and plugin by default", () => {
expect(shouldUseBundledTemplate("blank", {})).toBe(true);
expect(shouldUseBundledTemplate("plugin", {})).toBe(true);
// The CLI always passes commander's default branch; "main" must not
// force a download on its own.
expect(shouldUseBundledTemplate("blank", { branch: "main" })).toBe(true);
expect(shouldUseBundledTemplate("plugin", { branch: "main" })).toBe(true);
});

it("always resolves content templates live", () => {
expect(shouldUseBundledTemplate("blog", {})).toBe(false);
});

it("forces live resolution for every explicit source override", () => {
// A user passing --branch expects that branch's template, not the
// bundled copy silently substituted for it.
expect(shouldUseBundledTemplate("blank", { branch: "release" })).toBe(
false
);
expect(shouldUseBundledTemplate("plugin", { branch: "release" })).toBe(
false
);
expect(
shouldUseBundledTemplate("blank", { localTemplatePath: "/tmp/t" })
).toBe(false);
expect(shouldUseBundledTemplate("plugin", { useYalc: true })).toBe(false);
});
});
18 changes: 12 additions & 6 deletions packages/create-nextly-app/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Command } from "commander";

import { resolveProjectArg, validateProjectName } from "./cli-args";
import { createNextly } from "./create-nextly";
import { getAvailableTemplateNames } from "./lib/templates";
import type { DatabaseType, ProjectApproach, ProjectType } from "./types";

// Static require for package.json. Works because tsup adds the createRequire
Expand Down Expand Up @@ -33,11 +34,13 @@ program
"-y, --yes",
"Skip prompts and use defaults (blank template, SQLite, local storage)"
)
.option("-t, --template <template>", "Project template (blank, blog)")
.option(
"-a, --approach <approach>",
"Schema approach (code-first, visual)"
"-t, --template <template>",
// Derive the list from the registry so --help stays in sync with what
// validation accepts as new templates are registered.
`Project template (${getAvailableTemplateNames().join(", ")})`
)
.option("-a, --approach <approach>", "Schema approach (code-first, visual)")
.option("-d, --database <db>", "Database type (sqlite, postgresql, mysql)")
.option("-b, --branch <branch>", "Git branch for template download", "main")
.option(
Expand Down Expand Up @@ -93,15 +96,18 @@ Examples:
if (projectName) {
const nameError = validateProjectName(projectName);
if (nameError) {
console.error(`Error: Invalid project name '${projectName}'. ${nameError}.`);
console.error(
`Error: Invalid project name '${projectName}'. ${nameError}.`
);
process.exit(1);
}
}

const projectType = options.template as ProjectType | undefined;

// Validate --template flag
const validTypes = ["blank", "blog"];
// Validate --template flag against the template registry so newly
// registered templates are accepted without touching this file.
const validTypes = getAvailableTemplateNames();
if (projectType && !validTypes.includes(projectType)) {
console.error(
`Error: Template '${projectType}' is not available. Use: ${validTypes.join(", ")}`
Expand Down
63 changes: 51 additions & 12 deletions packages/create-nextly-app/src/create-nextly.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,11 @@ import {
cleanupDownload,
type TemplateSource,
} from "./lib/download-template";
import { templateHasApproaches, getDefaultApproach } from "./lib/templates";
import {
templateHasApproaches,
getDefaultApproach,
shouldUseBundledTemplate,
} from "./lib/templates";
import { getApproachPromptOptions } from "./prompts/approach";
import { DATABASE_CONFIGS, DATABASE_LABELS } from "./prompts/database";
import {
Expand All @@ -37,9 +41,10 @@ import type {
DatabaseConfig,
DatabaseType,
ProjectApproach,
ProjectInfo,
ProjectType,
} from "./types";
import { detectProject } from "./utils/detect";
import { detectPackageManager, detectProject } from "./utils/detect";
import { emptyDirectory, isDirectoryNotEmpty } from "./utils/fs";
import { copyTemplate } from "./utils/template";

Expand Down Expand Up @@ -213,6 +218,17 @@ export async function createNextly(
projectType = isValidTemplateSelection(template) ? template : "blank";
}

// The plugin template scaffolds a standalone publishable package. In an
// existing Next.js project the fresh-scaffold path is skipped entirely, so
// proceeding would install app dependencies and generate app config while
// never copying the plugin source — a broken hybrid. Fail fast instead.
if (projectType === "plugin" && !isFreshProject) {
p.cancel(
"The Plugin template creates a standalone package and cannot be installed into an existing Next.js project. Run create-nextly-app in an empty directory instead."
);
return;
Comment thread
aqib-rx marked this conversation as resolved.
Outdated
}

// --- Step 3: Schema approach (only for content templates with approaches) ---

let approach: ProjectApproach | undefined;
Expand Down Expand Up @@ -343,13 +359,17 @@ export async function createNextly(

try {
// Resolve template source (download from GitHub or use local path).
// For "blank" template without --local-template, we use the bundled
// fallback embedded in the CLI. For content templates (blog) we
// resolve via GitHub or a local path. --use-yalc also triggers
// resolution for the blank template so local-dev runs always pair
// yalc-linked packages with the live template rather than a
// potentially-stale bundled copy.
if (projectType !== "blank" || options.localTemplatePath || useYalc) {
// Bundled templates (blank, plugin) scaffold from the copy shipped
// inside the CLI package by default; content templates (blog) and any
// explicit source override (--local-template, --use-yalc, a non-main
// --branch) resolve live. See shouldUseBundledTemplate for the rules.
if (
!shouldUseBundledTemplate(projectType, {
localTemplatePath: options.localTemplatePath,
useYalc,
branch: options.branch,
})
) {
s.start("Resolving template...");
templateSource = await resolveTemplateSource(projectType, {
localTemplatePath: options.localTemplatePath,
Expand Down Expand Up @@ -420,8 +440,27 @@ export async function createNextly(
const s = p.spinner();
s.start("Detecting project...");
try {
projectInfo = await detectProject(cwd);
s.stop(`Detected Next.js ${projectInfo.nextVersion || "unknown"}`);
if (projectType === "plugin") {
Comment thread
aqib-rx marked this conversation as resolved.
// A plugin scaffold is a publishable library, not a Next.js app: the
// library code lives at src/ and the Next app lives in dev/, so the
// app-shaped detection would reject it ("App Router not detected").
// Downstream only consumes packageManager for fresh scaffolds
// (install + next-steps), so detect that and describe the fixed
// plugin layout statically.
projectInfo = {
isNextJs: false,
isAppRouter: false,
hasTypescript: true,
packageManager: await detectPackageManager(cwd),
nextVersion: null,
srcDir: true,
appDir: "dev/src/app",
} satisfies ProjectInfo;
s.stop("Detected plugin package");
} else {
projectInfo = await detectProject(cwd);
s.stop(`Detected Next.js ${projectInfo.nextVersion || "unknown"}`);
}
} catch (error) {
s.stop("Detection failed");
telemetry.capture("scaffold_failed", {
Expand Down Expand Up @@ -527,7 +566,7 @@ export async function createNextly(
);
lines.push("");
lines.push(
` Open ${pc.cyan("http://localhost:3000/admin")} — your plugin is registered.`
` Open ${pc.cyan("http://localhost:3000/admin")} — auto-logged-in with your plugin registered.`
);
lines.push(
` Edit your plugin in ${pc.dim("src/")}; the ${pc.dim("dev/")} app is never published.`
Expand Down
5 changes: 5 additions & 0 deletions packages/create-nextly-app/src/installers/dependencies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,11 @@ export async function installDependencies(
"@nextlyhq/adapter-drizzle",
...ALL_ADAPTER_PACKAGES,
...pluginPackages,
// A plugin scaffold imports @nextlyhq/plugin-sdk directly (its
// definePlugin surface + test harness). Linking it from the yalc
// store keeps it in lockstep with the other local packages instead
// of mixing the published SDK into a local-package scaffold.
...(projectType === "plugin" ? ["@nextlyhq/plugin-sdk"] : []),
Comment thread
aqib-rx marked this conversation as resolved.
Outdated
]),
];

Expand Down
33 changes: 33 additions & 0 deletions packages/create-nextly-app/src/lib/templates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,3 +96,36 @@ export function getDefaultApproach(name: string): ProjectApproach {
const template = getTemplate(name);
return template?.defaultApproach ?? "code-first";
}

/**
* Templates whose files ship inside the CLI package itself (copied by the
* build; see tsup.config.ts). Content templates (blog) are download-only.
*/
const BUNDLED_TEMPLATES: ReadonlySet<string> = new Set(["blank", "plugin"]);
Comment thread
aqib-rx marked this conversation as resolved.

/**
* Decide whether a scaffold should use the CLI-bundled copy of a template
* instead of resolving one from GitHub or a local path.
*
* Bundled copies keep offline scaffolds working and always match the
* installed CLI's package versions (a GitHub-main template can drift ahead
* of the npm packages the scaffold installs). Any explicit source override
* — --local-template, --use-yalc, or a non-main --branch — forces live
* resolution so development workflows pair live templates with local
* packages and users can scaffold from release branches.
*/
export function shouldUseBundledTemplate(
projectType: string,
options: {
localTemplatePath?: string;
useYalc?: boolean;
branch?: string;
}
): boolean {
return (
BUNDLED_TEMPLATES.has(projectType) &&
!options.localTemplatePath &&
!options.useYalc &&
(options.branch ?? "main") === "main"
);
}
Loading
Loading