-
Notifications
You must be signed in to change notification settings - Fork 0
feat(create-ideal-cms): make scaffolded projects installable #95
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
c7f0382
f6c3b2b
ef5e7d1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -36,6 +36,7 @@ | |
| "@focus-reactive/payload-plugin-scheduling": "workspace:*", | ||
| "@focus-reactive/payload-plugin-seo": "workspace:*", | ||
| "@focus-reactive/payload-plugin-translator": "workspace:*", | ||
| "@fr-private/payload-plugin-releases": "^0.1.1", | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Adding I confirmed this by regenerating locally — the diff is real and non-trivial (adds Consequences: any Note the local regeneration also removed |
||
| "@fr-private/payload-plugin-visual-editing": "^1.0.2", | ||
| "@payloadcms/db-postgres": "3.84.1", | ||
| "@payloadcms/live-preview-react": "3.84.1", | ||
|
|
||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| import { contentReleasesPlugin } from "@fr-private/payload-plugin-releases"; | ||
| import { visualEditingPlugin } from "@fr-private/payload-plugin-visual-editing"; | ||
| import type { Plugin } from "payload"; | ||
|
|
||
| // Plugins published to the private `@fr-private` npm scope. They are grouped | ||
| // here — instead of inline in `index.ts` — so `create-ideal-cms` can replace | ||
| // THIS FILE with an empty list when scaffolding a project for someone without | ||
| // access to that scope (see packages/create-ideal-cms/src/stubs.ts). | ||
| export const privatePlugins: Plugin[] = [ | ||
| visualEditingPlugin({ | ||
| adminBasePath: "/admin", | ||
| skipCollections: [ | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Today An allowlist ( |
||
| "users", | ||
| "media", | ||
| "categories", | ||
| "authors", | ||
| "testimonials", | ||
| "header", | ||
| "footer", | ||
| "document-embeddings", | ||
| "redirects", | ||
| "presets", | ||
| "comments", | ||
| "comment-reads", | ||
| "ab-experiments", | ||
| "payload-mcp-api-keys", | ||
| ], | ||
| skipGlobals: ["site-settings"], | ||
| }), | ||
|
|
||
| contentReleasesPlugin({ | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The plugin returns
Result: on any environment, opening the Releases admin view or hitting Run |
||
| // Batch content publishing: group document changes into a release and | ||
| // publish them together. Scoped to the same collections as our other | ||
| // content plugins. | ||
| enabledCollections: ["page", "posts"], | ||
| // Scheduled releases are not wired up (no cron endpoint / schedulerSecret), | ||
| // so disable the built-in setInterval poller — it is pointless here and | ||
| // unreliable on serverless. Releases are published manually. | ||
| schedulerInterval: false, | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The plugin only registers But the collection still exposes Disabling the poller is the right call on serverless, but it needs a matching guard: either restrict the |
||
| }), | ||
| ]; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| // Client-side surface of the visual-editing plugin, re-exported through the | ||
| // app so frontend components never import `@fr-private/*` directly. | ||
| // | ||
| // The package is published to a private npm scope, so `create-ideal-cms` | ||
| // replaces THIS FILE with inert no-op equivalents when scaffolding a project | ||
| // (see packages/create-ideal-cms/src/stubs.ts). Keep the exported surface | ||
| // minimal and keep the stub in sync with it. | ||
| export { | ||
| VisualEditing, | ||
| withVisualEditingPath, | ||
| } from "@fr-private/payload-plugin-visual-editing/client"; |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -1,20 +1,35 @@ | ||||||
| import { downloadTemplate } from "giget"; | ||||||
| import { copyFile, mkdir, readdir, readlink, rm, symlink, writeFile } from "node:fs/promises"; | ||||||
| import { | ||||||
| copyFile, | ||||||
| mkdir, | ||||||
| readdir, | ||||||
| readFile, | ||||||
| readlink, | ||||||
| rm, | ||||||
| symlink, | ||||||
| writeFile, | ||||||
| } from "node:fs/promises"; | ||||||
| import { join } from "node:path"; | ||||||
|
|
||||||
| export type TemplateSource = { kind: "github"; ref: string } | { kind: "local"; path: string }; | ||||||
|
|
||||||
| /** Maps published package name -> version, harvested from the template before pruning. */ | ||||||
| export type PluginVersions = Record<string, string>; | ||||||
|
|
||||||
| // Every `packages/<PLUGIN_DIR_PREFIX>*` directory is pruned: users consume the | ||||||
| // plugins as published npm packages instead of workspace source. Matching by | ||||||
| // prefix rather than an explicit list means a newly added plugin can never be | ||||||
| // forgotten here and leak into scaffolded projects. | ||||||
| const PLUGIN_DIR_PREFIX = "payload-plugin-"; | ||||||
|
|
||||||
| // `apps/cms` is the only app a scaffolded project gets. Everything else under | ||||||
| // `apps/` is a source-monorepo sandbox or demo (`dev`, `multi-tenancy-demo`, …) | ||||||
| // and is pruned by exclusion, so new sandboxes can't leak into output either. | ||||||
| const KEPT_APP = "cms"; | ||||||
|
|
||||||
| const PRUNE_PATHS = [ | ||||||
| // Plugin source — users consume them as published npm packages instead. | ||||||
| "packages/payload-plugin-ab", | ||||||
| "packages/payload-plugin-comments", | ||||||
| "packages/payload-plugin-presets", | ||||||
| "packages/payload-plugin-scheduling", | ||||||
| "packages/payload-plugin-translator", | ||||||
| // The CLI itself — its source lives inside the monorepo and must not bleed into scaffolded projects. | ||||||
| "packages/create-ideal-cms", | ||||||
| // The plugin dev sandbox app — only useful inside the source monorepo. | ||||||
| "apps/dev", | ||||||
| // Release machinery — not relevant downstream. | ||||||
| ".releaserc.json", | ||||||
| ".github", | ||||||
|
|
@@ -23,6 +38,10 @@ const PRUNE_PATHS = [ | |||||
| "docs", | ||||||
| "CLAUDE.md", | ||||||
| ".cursor", | ||||||
| // Agent config: repo-specific skills/subagents, plus `.claude/worktrees/`, | ||||||
| // which holds entire extra checkouts of this monorepo. | ||||||
| ".claude", | ||||||
| ".agents", | ||||||
| // Lockfile gets regenerated by the user's package manager. | ||||||
| "bun.lock", | ||||||
| // Any caches / build artifacts that may have slipped through. | ||||||
|
|
@@ -45,6 +64,10 @@ const LOCAL_COPY_SKIP = new Set([ | |||||
| ".git", | ||||||
| ".DS_Store", | ||||||
| ".test-output", | ||||||
| // Pruned anyway, but skipped up front so `.claude/worktrees/` — which can hold | ||||||
| // several full checkouts of this monorepo — is never copied in the first place. | ||||||
| ".claude", | ||||||
| ".agents", | ||||||
| ]); | ||||||
|
|
||||||
| const MIGRATION_STUB = `// Regenerated by create-ideal-cms. | ||||||
|
|
@@ -91,7 +114,57 @@ async function resetMigrations(targetDir: string): Promise<void> { | |||||
| await writeFile(join(dir, "index.ts"), MIGRATION_STUB); | ||||||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This is a pre-existing bug, but it sits inside
Suggested change
(the real fix is one line up: |
||||||
| } | ||||||
|
|
||||||
| export async function fetchTemplate(targetDir: string, source: TemplateSource): Promise<void> { | ||||||
| /** | ||||||
| * Lists `packages/payload-plugin-*` dirs and reads each one's published name + | ||||||
| * version. Must run BEFORE the prune — the versions are the only reason those | ||||||
| * directories are fetched at all. Dirs without a readable `package.json` (stray | ||||||
| * build artifacts) contribute nothing but are still returned for pruning. | ||||||
| */ | ||||||
| async function collectPluginPackages( | ||||||
| targetDir: string | ||||||
| ): Promise<{ dirs: string[]; versions: PluginVersions }> { | ||||||
| const packagesDir = join(targetDir, "packages"); | ||||||
| let entries: string[]; | ||||||
| try { | ||||||
| entries = (await readdir(packagesDir, { withFileTypes: true })) | ||||||
| .filter((e) => e.isDirectory() && e.name.startsWith(PLUGIN_DIR_PREFIX)) | ||||||
| .map((e) => e.name); | ||||||
| } catch { | ||||||
| return { dirs: [], versions: {} }; | ||||||
| } | ||||||
|
|
||||||
| const versions: PluginVersions = {}; | ||||||
| await Promise.all( | ||||||
| entries.map(async (name) => { | ||||||
| try { | ||||||
| const pkg = JSON.parse( | ||||||
| await readFile(join(packagesDir, name, "package.json"), "utf-8") | ||||||
| ) as { name?: string; version?: string; private?: boolean }; | ||||||
| if (pkg.name && pkg.version && pkg.private !== true) versions[pkg.name] = pkg.version; | ||||||
| } catch { | ||||||
| // No package.json / unparsable — not a real workspace package, prune only. | ||||||
| } | ||||||
| }) | ||||||
| ); | ||||||
|
|
||||||
| return { dirs: entries.map((name) => join("packages", name)), versions }; | ||||||
| } | ||||||
|
|
||||||
| /** Every `apps/*` dir except the one app a scaffolded project keeps. */ | ||||||
| async function collectExtraAppDirs(targetDir: string): Promise<string[]> { | ||||||
| try { | ||||||
| return (await readdir(join(targetDir, "apps"), { withFileTypes: true })) | ||||||
| .filter((e) => e.isDirectory() && e.name !== KEPT_APP) | ||||||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This is reachable via the local path:
Suggested change
|
||||||
| .map((e) => join("apps", e.name)); | ||||||
| } catch { | ||||||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Swallowing the Before this PR, The same
|
||||||
| return []; | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| export async function fetchTemplate( | ||||||
| targetDir: string, | ||||||
| source: TemplateSource | ||||||
| ): Promise<PluginVersions> { | ||||||
| if (source.kind === "local") { | ||||||
| await copyLocal(source.path, targetDir); | ||||||
| } else { | ||||||
|
|
@@ -103,9 +176,18 @@ export async function fetchTemplate(targetDir: string, source: TemplateSource): | |||||
| }); | ||||||
| } | ||||||
|
|
||||||
| const [{ dirs, versions }, extraApps] = await Promise.all([ | ||||||
| collectPluginPackages(targetDir), | ||||||
| collectExtraAppDirs(targetDir), | ||||||
| ]); | ||||||
|
|
||||||
| await Promise.all( | ||||||
| PRUNE_PATHS.map((path) => rm(join(targetDir, path), { recursive: true, force: true })) | ||||||
| [...PRUNE_PATHS, ...dirs, ...extraApps].map((path) => | ||||||
| rm(join(targetDir, path), { recursive: true, force: true }) | ||||||
| ) | ||||||
| ); | ||||||
|
|
||||||
| await resetMigrations(targetDir); | ||||||
|
|
||||||
| return versions; | ||||||
| } | ||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Three sentences on one physical line — violates the Markdown convention in
~/.claude/CLAUDE.md.Note this cannot be fixed in isolation: splitting the sentences breaks the
LINE_STRIP_FILESentry forapps/cms/CLAUDE.mdinpackages/create-ideal-cms/src/stubs.ts, which drops whole lines matching@fr-privateand would leave the trailing sentences orphaned in scaffolded output. Fix the strip mechanism first (see the comment onstubs.ts), then reformat.