bsmnt 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.changeset/2026-02-11-test-patch-bump.md +5 -0
- package/.changeset/README.md +10 -0
- package/.changeset/config.json +16 -0
- package/.cursor/rules/README.md +184 -0
- package/.cursor/rules/architecture.mdc +437 -0
- package/.cursor/rules/components.mdc +436 -0
- package/.cursor/rules/integrations.mdc +447 -0
- package/.cursor/rules/main.mdc +278 -0
- package/.cursor/rules/styling.mdc +433 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +14 -0
- package/.github/workflows/.gitkeep +0 -0
- package/.github/workflows/ci.yml +37 -0
- package/.github/workflows/release.yml +54 -0
- package/.tldr/cache/call_graph.json +7 -0
- package/.tldr/languages.json +6 -0
- package/.tldr/status +1 -0
- package/.tldrignore +84 -0
- package/.vscode/extensions.json +20 -0
- package/.vscode/settings.json +98 -0
- package/CHANGELOG.md +13 -0
- package/CLAUDE.md +138 -0
- package/README.md +176 -0
- package/bin/index.js +262 -0
- package/biome.json +44 -0
- package/bun.lock +496 -0
- package/changelog/04-02-26.md +86 -0
- package/changelog/05-02-26.md +101 -0
- package/changelog/09-02-26.md +83 -0
- package/docs/fix-studio-hydration.md +46 -0
- package/docs/plans/2026-01-29-sanity-smart-merge-design.md +196 -0
- package/docs/plans/2026-01-29-sanity-smart-merge-implementation.md +695 -0
- package/docs/sanity-setup-steps.md +199 -0
- package/integrations/basehub/README.md +3 -0
- package/integrations/sanity/app/api/draft-mode/disable/route.ts +7 -0
- package/integrations/sanity/app/api/draft-mode/enable/route.ts +21 -0
- package/integrations/sanity/app/api/revalidate/route.ts +37 -0
- package/integrations/sanity/app/layout.tsx +111 -0
- package/integrations/sanity/app/sitemap.ts +80 -0
- package/integrations/sanity/app/studio/[[...tool]]/page.tsx +8 -0
- package/integrations/sanity/app/studio/layout.tsx +7 -0
- package/integrations/sanity/components/ui/sanity-image/index.tsx +37 -0
- package/integrations/sanity/lib/integrations/README.md +58 -0
- package/integrations/sanity/lib/integrations/check-integration.ts +62 -0
- package/integrations/sanity/lib/integrations/sanity/README.md +144 -0
- package/integrations/sanity/lib/integrations/sanity/client.ts +30 -0
- package/integrations/sanity/lib/integrations/sanity/components/disable-draft-mode.tsx +29 -0
- package/integrations/sanity/lib/integrations/sanity/components/rich-text.tsx +73 -0
- package/integrations/sanity/lib/integrations/sanity/env.ts +38 -0
- package/integrations/sanity/lib/integrations/sanity/live/index.tsx +34 -0
- package/integrations/sanity/lib/integrations/sanity/queries.ts +99 -0
- package/integrations/sanity/lib/integrations/sanity/sanity.cli.ts +20 -0
- package/integrations/sanity/lib/integrations/sanity/sanity.config.ts +94 -0
- package/integrations/sanity/lib/integrations/sanity/sanity.types.ts +337 -0
- package/integrations/sanity/lib/integrations/sanity/schema.json +1850 -0
- package/integrations/sanity/lib/integrations/sanity/schemas/article.ts +132 -0
- package/integrations/sanity/lib/integrations/sanity/schemas/example.ts +203 -0
- package/integrations/sanity/lib/integrations/sanity/schemas/index.ts +37 -0
- package/integrations/sanity/lib/integrations/sanity/schemas/link.ts +127 -0
- package/integrations/sanity/lib/integrations/sanity/schemas/metadata.ts +68 -0
- package/integrations/sanity/lib/integrations/sanity/schemas/navigation.ts +39 -0
- package/integrations/sanity/lib/integrations/sanity/schemas/page.ts +77 -0
- package/integrations/sanity/lib/integrations/sanity/schemas/richText.ts +59 -0
- package/integrations/sanity/lib/integrations/sanity/structure.ts +5 -0
- package/integrations/sanity/lib/integrations/sanity/utils/image.ts +11 -0
- package/integrations/sanity/lib/integrations/sanity/utils/link.ts +61 -0
- package/integrations/sanity/lib/scripts/copy-sanity-mcp.ts +23 -0
- package/integrations/sanity/lib/scripts/generate-page.ts +310 -0
- package/integrations/sanity/lib/utils/metadata.ts +190 -0
- package/layers/experiment/components/layout/header/index.tsx +58 -0
- package/layers/experiment/components/layout/navigation-menu.tsx +127 -0
- package/layers/experiment/lib/constants.ts +12 -0
- package/layers/webgl/app/page.tsx +10 -0
- package/layers/webgl/components/webgl/canvas/dynamic.tsx +34 -0
- package/layers/webgl/components/webgl/canvas/index.tsx +43 -0
- package/layers/webgl/components/webgl/components/scene/index.tsx +21 -0
- package/layers/webgpu/.gitkeep +0 -0
- package/package.json +44 -0
- package/plugins/README.md +21 -0
- package/plugins/no-anchor-element.grit +11 -0
- package/plugins/no-relative-parent-imports.grit +6 -0
- package/plugins/no-unnecessary-forwardref.grit +5 -0
- package/src/commands/add-integration.js +325 -0
- package/src/commands/create.js +415 -0
- package/src/commands/setup-sanity.js +426 -0
- package/src/commands/worktree.js +805 -0
- package/src/mergers/check-integration-merger.js +105 -0
- package/src/mergers/config.js +137 -0
- package/src/mergers/index.js +355 -0
- package/src/mergers/layout-merger.js +223 -0
- package/src/mergers/next-config-merger.js +63 -0
- package/src/mergers/sitemap-merger.js +121 -0
- package/tasks/prd-next-starter-dynamic-layers.md +184 -0
- package/tasks/prd.json +153 -0
- package/tasks/progress.txt +115 -0
- package/template-hooks/use-battery.ts +126 -0
- package/template-hooks/use-device-perf.ts +184 -0
- package/template-hooks/use-intersection-observer.ts +32 -0
- package/template-hooks/use-media.ts +33 -0
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
// src/mergers/layout-merger.js
|
|
2
|
+
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import fs from "fs-extra";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Sanity-specific imports to inject into layout.
|
|
8
|
+
* Only two lightweight components — no dynamic functions (draftMode, headers, etc.)
|
|
9
|
+
*/
|
|
10
|
+
const SANITY_IMPORTS = `import { SanityStudioGuard } from "@/components/sanity/studio-guard"
|
|
11
|
+
import { SanityVisualEditing } from "@/components/sanity/visual-editing"`;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Client component that hides site chrome on /studio routes.
|
|
15
|
+
* Uses useSelectedLayoutSegment() — a stable Next.js client API.
|
|
16
|
+
*/
|
|
17
|
+
const STUDIO_GUARD_COMPONENT = `"use client"
|
|
18
|
+
|
|
19
|
+
import { useSelectedLayoutSegment } from "next/navigation"
|
|
20
|
+
|
|
21
|
+
export function SanityStudioGuard({ children }: { children: React.ReactNode }) {
|
|
22
|
+
const segment = useSelectedLayoutSegment()
|
|
23
|
+
if (segment === "studio") return null
|
|
24
|
+
return <>{children}</>
|
|
25
|
+
}
|
|
26
|
+
`;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Server component that handles Sanity visual editing.
|
|
30
|
+
* Keeps draftMode() inside its own <Suspense> boundary so the root layout
|
|
31
|
+
* never calls dynamic functions directly — preserving static rendering.
|
|
32
|
+
*/
|
|
33
|
+
const VISUAL_EDITING_COMPONENT = `import { Suspense } from "react"
|
|
34
|
+
import { draftMode } from "next/headers"
|
|
35
|
+
import { VisualEditing } from "next-sanity/visual-editing"
|
|
36
|
+
import { isSanityConfigured } from "@/lib/integrations/check-integration"
|
|
37
|
+
import { SanityLive } from "@/lib/integrations/sanity/live"
|
|
38
|
+
|
|
39
|
+
async function VisualEditingInner() {
|
|
40
|
+
const { isEnabled: isDraftMode } = await draftMode()
|
|
41
|
+
const sanityConfigured = isSanityConfigured()
|
|
42
|
+
|
|
43
|
+
if (!sanityConfigured || !isDraftMode) return null
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<>
|
|
47
|
+
<VisualEditing />
|
|
48
|
+
<SanityLive />
|
|
49
|
+
</>
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function SanityVisualEditing() {
|
|
54
|
+
return (
|
|
55
|
+
<Suspense fallback={null}>
|
|
56
|
+
<VisualEditingInner />
|
|
57
|
+
</Suspense>
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
`;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Merge Sanity integration into template layout.
|
|
64
|
+
* The layout itself stays free of dynamic functions (draftMode, headers, etc.)
|
|
65
|
+
* All dynamic behavior is encapsulated in generated components.
|
|
66
|
+
*
|
|
67
|
+
* Strategy:
|
|
68
|
+
* 1. Inject two imports (SanityStudioGuard, SanityVisualEditing)
|
|
69
|
+
* 2. Wrap body chrome in <SanityStudioGuard>
|
|
70
|
+
* 3. Add <SanityVisualEditing /> before </body>
|
|
71
|
+
* 4. Generate the two component files
|
|
72
|
+
*
|
|
73
|
+
* @param {string} templatePath - Path to the template's layout.tsx
|
|
74
|
+
* @param {object} options - Options from the merge orchestrator
|
|
75
|
+
* @param {string} options.targetDir - The project root directory
|
|
76
|
+
* @param {string} options.pathPrefix - Path prefix (e.g., 'src/' or '')
|
|
77
|
+
* @returns {Promise<{skipped?: boolean, reason?: string, success?: boolean}>}
|
|
78
|
+
*/
|
|
79
|
+
export async function mergeLayout(
|
|
80
|
+
templatePath,
|
|
81
|
+
{ targetDir, pathPrefix } = {},
|
|
82
|
+
) {
|
|
83
|
+
let content = await fs.readFile(templatePath, "utf-8");
|
|
84
|
+
|
|
85
|
+
// Skip if already has Sanity integration
|
|
86
|
+
if (
|
|
87
|
+
content.includes("SanityStudioGuard") ||
|
|
88
|
+
content.includes("isSanityConfigured")
|
|
89
|
+
) {
|
|
90
|
+
return { skipped: true, reason: "Already has Sanity integration" };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// 1. Add Sanity imports after existing imports
|
|
94
|
+
const importMatches = [...content.matchAll(/^import .+$/gm)];
|
|
95
|
+
if (importMatches.length > 0) {
|
|
96
|
+
const lastImport = importMatches[importMatches.length - 1];
|
|
97
|
+
const insertPos = lastImport.index + lastImport[0].length;
|
|
98
|
+
content =
|
|
99
|
+
content.slice(0, insertPos) +
|
|
100
|
+
"\n" +
|
|
101
|
+
SANITY_IMPORTS +
|
|
102
|
+
content.slice(insertPos);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// 2. Ensure Suspense is imported from react
|
|
106
|
+
if (!content.match(/import\s+.*Suspense.*from\s+["']react["']/)) {
|
|
107
|
+
const reactImportMatch = content.match(
|
|
108
|
+
/import\s+\{([^}]*)\}\s+from\s+["']react["']/,
|
|
109
|
+
);
|
|
110
|
+
if (reactImportMatch) {
|
|
111
|
+
if (!reactImportMatch[1].includes("Suspense")) {
|
|
112
|
+
content = content.replace(
|
|
113
|
+
reactImportMatch[0],
|
|
114
|
+
reactImportMatch[0].replace(
|
|
115
|
+
reactImportMatch[1],
|
|
116
|
+
`${reactImportMatch[1].trim()}, Suspense`,
|
|
117
|
+
),
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
} else {
|
|
121
|
+
const lastImportMatch = content.match(/^(import\s+.+\n)/gm);
|
|
122
|
+
if (lastImportMatch) {
|
|
123
|
+
const lastImport = lastImportMatch[lastImportMatch.length - 1];
|
|
124
|
+
content = content.replace(
|
|
125
|
+
lastImport,
|
|
126
|
+
`${lastImport}import { Suspense } from "react"\n`,
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// 3. Wrap body chrome in <SanityStudioGuard> and add <SanityVisualEditing />
|
|
133
|
+
content = content.replace(
|
|
134
|
+
/(\s*)(<body[^>]*>)([\s\S]*?)(\{children\})([\s\S]*?)(<\/body>)/m,
|
|
135
|
+
(
|
|
136
|
+
_match,
|
|
137
|
+
bodyIndent,
|
|
138
|
+
bodyOpen,
|
|
139
|
+
beforeChildren,
|
|
140
|
+
_childrenExpr,
|
|
141
|
+
afterChildren,
|
|
142
|
+
bodyClose,
|
|
143
|
+
) => {
|
|
144
|
+
const baseIndent = bodyIndent || " ";
|
|
145
|
+
const indent = `${baseIndent} `;
|
|
146
|
+
const innerIndent = `${indent} `;
|
|
147
|
+
|
|
148
|
+
const trimmedBefore = beforeChildren.trim();
|
|
149
|
+
const trimmedAfter = afterChildren.trim();
|
|
150
|
+
const hasChrome = trimmedBefore.length > 0 || trimmedAfter.length > 0;
|
|
151
|
+
|
|
152
|
+
// Preserve relative indentation of chrome content
|
|
153
|
+
const reindent = (text, targetIndent) => {
|
|
154
|
+
const lines = text.split("\n").filter((line) => line.trim());
|
|
155
|
+
if (lines.length === 0) return "";
|
|
156
|
+
const minIndent = Math.min(
|
|
157
|
+
...lines
|
|
158
|
+
.map((line) => line.match(/^(\s*)/)[1].length)
|
|
159
|
+
.filter((n) => n > 0),
|
|
160
|
+
Infinity,
|
|
161
|
+
);
|
|
162
|
+
return lines
|
|
163
|
+
.map((line) => {
|
|
164
|
+
const currentIndent = line.match(/^(\s*)/)[1].length;
|
|
165
|
+
const relativeIndent = Math.max(
|
|
166
|
+
0,
|
|
167
|
+
currentIndent - (minIndent === Infinity ? 0 : minIndent),
|
|
168
|
+
);
|
|
169
|
+
return targetIndent + " ".repeat(relativeIndent) + line.trim();
|
|
170
|
+
})
|
|
171
|
+
.join("\n");
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
let result = `${baseIndent}${bodyOpen}\n`;
|
|
175
|
+
|
|
176
|
+
if (hasChrome && trimmedBefore) {
|
|
177
|
+
result += `${indent}<Suspense fallback={null}>\n`;
|
|
178
|
+
result += `${innerIndent}<SanityStudioGuard>\n`;
|
|
179
|
+
result += reindent(trimmedBefore, `${innerIndent} `);
|
|
180
|
+
result += `\n${innerIndent}</SanityStudioGuard>\n`;
|
|
181
|
+
result += `${indent}</Suspense>\n\n`;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
result += `${indent}{children}\n`;
|
|
185
|
+
|
|
186
|
+
if (hasChrome && trimmedAfter) {
|
|
187
|
+
result += `\n${indent}<Suspense fallback={null}>\n`;
|
|
188
|
+
result += `${innerIndent}<SanityStudioGuard>\n`;
|
|
189
|
+
result += reindent(trimmedAfter, `${innerIndent} `);
|
|
190
|
+
result += `\n${innerIndent}</SanityStudioGuard>\n`;
|
|
191
|
+
result += `${indent}</Suspense>\n`;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
result += `\n${indent}<SanityVisualEditing />\n${baseIndent}${bodyClose}`;
|
|
195
|
+
return result;
|
|
196
|
+
},
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
await fs.writeFile(templatePath, content);
|
|
200
|
+
|
|
201
|
+
// 3. Generate component files
|
|
202
|
+
if (targetDir) {
|
|
203
|
+
const sanityDir = path.join(
|
|
204
|
+
targetDir,
|
|
205
|
+
pathPrefix || "",
|
|
206
|
+
"components",
|
|
207
|
+
"sanity",
|
|
208
|
+
);
|
|
209
|
+
await fs.ensureDir(sanityDir);
|
|
210
|
+
|
|
211
|
+
const guardPath = path.join(sanityDir, "studio-guard.tsx");
|
|
212
|
+
if (!(await fs.pathExists(guardPath))) {
|
|
213
|
+
await fs.writeFile(guardPath, STUDIO_GUARD_COMPONENT);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const visualEditingPath = path.join(sanityDir, "visual-editing.tsx");
|
|
217
|
+
if (!(await fs.pathExists(visualEditingPath))) {
|
|
218
|
+
await fs.writeFile(visualEditingPath, VISUAL_EDITING_COMPONENT);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return { success: true };
|
|
223
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// src/mergers/next-config-merger.js
|
|
2
|
+
import fs from "fs-extra";
|
|
3
|
+
|
|
4
|
+
const SANITY_PACKAGES =
|
|
5
|
+
'"@sanity/client", "@sanity/image-url", "@sanity/asset-utils"';
|
|
6
|
+
|
|
7
|
+
const IMAGES_BLOCK = `images: {
|
|
8
|
+
remotePatterns: [
|
|
9
|
+
{
|
|
10
|
+
protocol: "https",
|
|
11
|
+
hostname: "cdn.sanity.io",
|
|
12
|
+
},
|
|
13
|
+
],
|
|
14
|
+
},`;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Merge Sanity-specific config into template next.config.ts
|
|
18
|
+
* Adds images.remotePatterns for cdn.sanity.io and
|
|
19
|
+
* experimental.optimizePackageImports for Sanity packages
|
|
20
|
+
*
|
|
21
|
+
* @param {string} templatePath - Path to the template's next.config.ts
|
|
22
|
+
* @returns {Promise<{skipped?: boolean, reason?: string, success?: boolean}>}
|
|
23
|
+
*/
|
|
24
|
+
export async function mergeNextConfig(templatePath) {
|
|
25
|
+
let content = await fs.readFile(templatePath, "utf-8");
|
|
26
|
+
|
|
27
|
+
// Skip if already has Sanity config
|
|
28
|
+
if (content.includes("cdn.sanity.io")) {
|
|
29
|
+
return { skipped: true, reason: "Already has Sanity image config" };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Match the NextConfig object opening: `const nextConfig: NextConfig = {`
|
|
33
|
+
const configObjectPattern = /(const\s+\w+\s*:\s*NextConfig\s*=\s*\{)/;
|
|
34
|
+
|
|
35
|
+
// 1. Add images.remotePatterns for Sanity CDN
|
|
36
|
+
if (!content.includes("remotePatterns")) {
|
|
37
|
+
content = content.replace(configObjectPattern, `$1\n ${IMAGES_BLOCK}`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// 2. Add optimizePackageImports for Sanity packages
|
|
41
|
+
if (content.includes("optimizePackageImports")) {
|
|
42
|
+
// Already has optimizePackageImports array — prepend Sanity packages
|
|
43
|
+
content = content.replace(
|
|
44
|
+
/(optimizePackageImports\s*:\s*\[)/,
|
|
45
|
+
`$1${SANITY_PACKAGES}, `,
|
|
46
|
+
);
|
|
47
|
+
} else if (content.includes("experimental")) {
|
|
48
|
+
// Has experimental block but no optimizePackageImports — add it inside
|
|
49
|
+
content = content.replace(
|
|
50
|
+
/(experimental\s*:\s*\{)/,
|
|
51
|
+
`$1\n optimizePackageImports: [${SANITY_PACKAGES}],`,
|
|
52
|
+
);
|
|
53
|
+
} else {
|
|
54
|
+
// No experimental block — add it to the config object
|
|
55
|
+
content = content.replace(
|
|
56
|
+
configObjectPattern,
|
|
57
|
+
`$1\n experimental: {\n optimizePackageImports: [${SANITY_PACKAGES}],\n },`,
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
await fs.writeFile(templatePath, content);
|
|
62
|
+
return { success: true };
|
|
63
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// src/mergers/sitemap-merger.js
|
|
2
|
+
import fs from "fs-extra";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Sanity-specific import to add
|
|
6
|
+
*/
|
|
7
|
+
const SANITY_IMPORT = `import { isSanityConfigured } from "@/lib/integrations/check-integration"`;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Sanity fetch logic to inject before return statement
|
|
11
|
+
* Uses early return pattern and spread operator for cleaner code
|
|
12
|
+
*/
|
|
13
|
+
const SANITY_FETCH_LOGIC = `
|
|
14
|
+
// Only fetch Sanity pages if Sanity is configured
|
|
15
|
+
if (isSanityConfigured()) {
|
|
16
|
+
try {
|
|
17
|
+
const sanityModule = await import("@/lib/integrations/sanity/client")
|
|
18
|
+
const sanityGroq = await import("next-sanity")
|
|
19
|
+
|
|
20
|
+
const client = sanityModule?.client
|
|
21
|
+
const groq = sanityGroq?.groq
|
|
22
|
+
|
|
23
|
+
// Skip if client is null (shouldn't happen since we check isSanityConfigured)
|
|
24
|
+
if (!(client && groq)) return baseRoutes
|
|
25
|
+
|
|
26
|
+
type SanityDocument = {
|
|
27
|
+
slug: { current: string }
|
|
28
|
+
_updatedAt: string
|
|
29
|
+
metadata?: { noIndex?: boolean }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Fetch all published pages and articles
|
|
33
|
+
const pages = (await client.fetch(
|
|
34
|
+
groq\`*[_type == "page" && defined(slug.current)] {
|
|
35
|
+
slug,
|
|
36
|
+
_updatedAt,
|
|
37
|
+
metadata
|
|
38
|
+
}\`
|
|
39
|
+
)) as SanityDocument[]
|
|
40
|
+
|
|
41
|
+
const articles = (await client.fetch(
|
|
42
|
+
groq\`*[_type == "article" && defined(slug.current)] {
|
|
43
|
+
slug,
|
|
44
|
+
_updatedAt,
|
|
45
|
+
metadata
|
|
46
|
+
}\`
|
|
47
|
+
)) as SanityDocument[]
|
|
48
|
+
|
|
49
|
+
// Add pages to sitemap (exclude noIndex pages)
|
|
50
|
+
const pageEntries: MetadataRoute.Sitemap = pages
|
|
51
|
+
.filter((page: SanityDocument) => !page.metadata?.noIndex)
|
|
52
|
+
.map((page: SanityDocument) => ({
|
|
53
|
+
url: \`\${APP_BASE_URL}/\${page.slug.current}\`,
|
|
54
|
+
lastModified: new Date(page._updatedAt),
|
|
55
|
+
changeFrequency: "weekly" as const,
|
|
56
|
+
priority: 0.8,
|
|
57
|
+
}))
|
|
58
|
+
|
|
59
|
+
// Add articles to sitemap (exclude noIndex articles)
|
|
60
|
+
const articleEntries: MetadataRoute.Sitemap = articles
|
|
61
|
+
.filter((article: SanityDocument) => !article.metadata?.noIndex)
|
|
62
|
+
.map((article: SanityDocument) => ({
|
|
63
|
+
url: \`\${APP_BASE_URL}/blog/\${article.slug.current}\`,
|
|
64
|
+
lastModified: new Date(article._updatedAt),
|
|
65
|
+
changeFrequency: "weekly" as const,
|
|
66
|
+
priority: 0.7,
|
|
67
|
+
}))
|
|
68
|
+
|
|
69
|
+
return [...baseRoutes, ...pageEntries, ...articleEntries]
|
|
70
|
+
} catch (error) {
|
|
71
|
+
console.error("Error generating sitemap from Sanity:", error)
|
|
72
|
+
return baseRoutes
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
`;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Merge Sanity integration into template sitemap
|
|
80
|
+
* Preserves all existing agnostic code (routes, baseRoutes structure)
|
|
81
|
+
* and injects Sanity-specific fetching logic
|
|
82
|
+
*
|
|
83
|
+
* @param {string} templatePath - Path to the template's sitemap.ts
|
|
84
|
+
* @returns {Promise<{skipped?: boolean, reason?: string, success?: boolean}>}
|
|
85
|
+
*/
|
|
86
|
+
export async function mergeSitemap(templatePath) {
|
|
87
|
+
let content = await fs.readFile(templatePath, "utf-8");
|
|
88
|
+
|
|
89
|
+
// Skip if already has Sanity integration
|
|
90
|
+
if (content.includes("isSanityConfigured")) {
|
|
91
|
+
return { skipped: true, reason: "Already has Sanity integration" };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// 1. Add Sanity import after existing imports
|
|
95
|
+
const importMatches = [...content.matchAll(/^import .+$/gm)];
|
|
96
|
+
if (importMatches.length > 0) {
|
|
97
|
+
const lastImport = importMatches[importMatches.length - 1];
|
|
98
|
+
const insertPos = lastImport.index + lastImport[0].length;
|
|
99
|
+
content =
|
|
100
|
+
content.slice(0, insertPos) +
|
|
101
|
+
"\n" +
|
|
102
|
+
SANITY_IMPORT +
|
|
103
|
+
content.slice(insertPos);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// 2. Add Sanity fetch logic before the final return statement
|
|
107
|
+
// Find the last "return baseRoutes" or "return [...baseRoutes" pattern
|
|
108
|
+
const returnMatch = content.match(
|
|
109
|
+
/(\n)([ \t]*)(return\s+(?:baseRoutes|\[\.\.\.baseRoutes))/m,
|
|
110
|
+
);
|
|
111
|
+
if (returnMatch) {
|
|
112
|
+
const returnIndex = content.indexOf(returnMatch[0]);
|
|
113
|
+
content =
|
|
114
|
+
content.slice(0, returnIndex) +
|
|
115
|
+
SANITY_FETCH_LOGIC +
|
|
116
|
+
content.slice(returnIndex);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
await fs.writeFile(templatePath, content);
|
|
120
|
+
return { success: true };
|
|
121
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# PRD: Replace Static Templates with next-starter + Dynamic Layers
|
|
2
|
+
|
|
3
|
+
## Introduction
|
|
4
|
+
|
|
5
|
+
The CLI currently maintains 4 complete, nearly-identical template copies (default, webgl, webgpu, experiment). Every bug fix, dependency bump, or structural change must be applied 4 times. The `default` template is essentially a stale snapshot of the `basementstudio/next-starter` repo.
|
|
6
|
+
|
|
7
|
+
This feature replaces the static templates with a single `next-starter` clone as the base, then dynamically overlays technology-specific files and dependencies using a "layers" system — reusing the same smart-merge pattern already proven by the CMS integration system.
|
|
8
|
+
|
|
9
|
+
## Goals
|
|
10
|
+
|
|
11
|
+
- Eliminate template duplication by using `basementstudio/next-starter` as the single source of truth
|
|
12
|
+
- Maintain the same user-facing CLI experience (same 4 template choices, same prompt flow)
|
|
13
|
+
- Create a `layers/` system for technology-specific additions (WebGL, WebGPU, Experiment)
|
|
14
|
+
- Reuse the existing merger infrastructure (`src/mergers/`) for layer injection
|
|
15
|
+
- Delete the entire `template/` directory from the repo
|
|
16
|
+
|
|
17
|
+
## User Stories
|
|
18
|
+
|
|
19
|
+
### US-001: Clone next-starter as base template
|
|
20
|
+
**Description:** As a developer using the CLI, I want my project to be scaffolded from the latest `next-starter` repo so that I always get the most up-to-date base configuration.
|
|
21
|
+
|
|
22
|
+
**Acceptance Criteria:**
|
|
23
|
+
- [ ] `create.js` clones from `github:basementstudio/next-starter#main` instead of `github:basementstudio/basement/template/{type}#main`
|
|
24
|
+
- [ ] Selecting "Default" template produces a project identical to cloning next-starter directly
|
|
25
|
+
- [ ] `bun.lock` is deleted after clone (since dependencies will be modified)
|
|
26
|
+
- [ ] Download spinner text updated to reflect new source
|
|
27
|
+
- [ ] Error/troubleshooting messages updated to reference `basementstudio/next-starter`
|
|
28
|
+
- [ ] Typecheck passes
|
|
29
|
+
|
|
30
|
+
### US-002: Create layer configuration system
|
|
31
|
+
**Description:** As a CLI maintainer, I want technology layers defined in a config file so that adding or modifying layers only requires changing one file.
|
|
32
|
+
|
|
33
|
+
**Acceptance Criteria:**
|
|
34
|
+
- [ ] `LAYER_CONFIG` export added to `src/mergers/config.js` alongside existing `CMS_CONFIG`
|
|
35
|
+
- [ ] Each layer config defines: `replaceFiles`, `additivePaths`, `dependencies`, `devDependencies`
|
|
36
|
+
- [ ] `getLayerConfig(layer)` helper function exported
|
|
37
|
+
- [ ] Config contains entries for `webgl`, `webgpu`, and `experiment`
|
|
38
|
+
- [ ] `webgpu` is a deps-only layer (empty `replaceFiles` and `additivePaths`)
|
|
39
|
+
- [ ] Dependencies match what's currently in each template's `package.template.json`
|
|
40
|
+
|
|
41
|
+
### US-003: Implement layer injection function
|
|
42
|
+
**Description:** As the CLI system, I need to overlay layer-specific files on top of the next-starter base so that WebGL/WebGPU/Experiment projects get their unique components.
|
|
43
|
+
|
|
44
|
+
**Acceptance Criteria:**
|
|
45
|
+
- [ ] `injectLayer(targetDir, layer, spinner)` function added to `src/mergers/index.js`
|
|
46
|
+
- [ ] Function copies additive files from local `layers/{type}/` directory (no tiged needed)
|
|
47
|
+
- [ ] Function replaces files listed in `replaceFiles` (overwrites base version)
|
|
48
|
+
- [ ] Function uses existing `detectPathPrefix()` and `transformPath()` for `src/` directory support
|
|
49
|
+
- [ ] Function returns results object with `replaced`, `copied`, `skipped`, `failed` arrays
|
|
50
|
+
- [ ] `formatMergeResults()` extended to handle `replaced` entries
|
|
51
|
+
- [ ] Skipped layers (type === 'default') produce no errors
|
|
52
|
+
|
|
53
|
+
### US-004: Extract layer files from templates
|
|
54
|
+
**Description:** As a CLI maintainer, I need the unique files from each template extracted into a `layers/` directory so they can be overlaid on next-starter.
|
|
55
|
+
|
|
56
|
+
**Acceptance Criteria:**
|
|
57
|
+
- [ ] `layers/webgl/app/page.tsx` contains the WebGL-specific page (DynamicCanvas import)
|
|
58
|
+
- [ ] `layers/webgl/components/webgl/canvas/dynamic.tsx` exists (from template/webgl)
|
|
59
|
+
- [ ] `layers/webgl/components/webgl/canvas/index.tsx` exists (from template/webgl)
|
|
60
|
+
- [ ] `layers/webgl/components/webgl/components/scene/index.tsx` exists (from template/webgl)
|
|
61
|
+
- [ ] `layers/experiment/components/layout/header/index.tsx` exists (custom header with NavigationMenu)
|
|
62
|
+
- [ ] `layers/experiment/components/layout/navigation-menu.tsx` exists
|
|
63
|
+
- [ ] `layers/experiment/lib/constats.ts` exists
|
|
64
|
+
- [ ] `layers/experiment/lib/utils/cn.ts` exists
|
|
65
|
+
- [ ] `layers/webgpu/` directory exists (empty, deps-only layer)
|
|
66
|
+
- [ ] Files are byte-for-byte identical to their template counterparts
|
|
67
|
+
|
|
68
|
+
### US-005: Integrate layer injection into create flow
|
|
69
|
+
**Description:** As the CLI system, I need the layer injection step wired into the project creation flow so it runs between template download and CMS integration.
|
|
70
|
+
|
|
71
|
+
**Acceptance Criteria:**
|
|
72
|
+
- [ ] Layer injection runs after next-starter clone, before CMS integration
|
|
73
|
+
- [ ] Layer injection only runs when `type !== 'default'`
|
|
74
|
+
- [ ] Progress spinner shows layer name during injection
|
|
75
|
+
- [ ] Injection results displayed to user (files added/replaced)
|
|
76
|
+
- [ ] Failures produce warnings, not hard errors
|
|
77
|
+
- [ ] CMS integration (Sanity) still works correctly after layer injection
|
|
78
|
+
- [ ] Typecheck passes
|
|
79
|
+
|
|
80
|
+
### US-006: Config-driven dependency injection
|
|
81
|
+
**Description:** As the CLI system, I need package.json hydration to read layer dependencies from config instead of being hardcoded in create.js.
|
|
82
|
+
|
|
83
|
+
**Acceptance Criteria:**
|
|
84
|
+
- [ ] Layer dependencies read from `LAYER_CONFIG` and merged into `package.json`
|
|
85
|
+
- [ ] CMS dependencies still injected as before (unchanged)
|
|
86
|
+
- [ ] Animation library dependencies still injected as before (unchanged)
|
|
87
|
+
- [ ] `package.template.json` rename logic simplified (next-starter uses `package.json` directly)
|
|
88
|
+
- [ ] Project name and version still set correctly
|
|
89
|
+
- [ ] Typecheck passes
|
|
90
|
+
|
|
91
|
+
### US-007: Delete template directory
|
|
92
|
+
**Description:** As a CLI maintainer, I want the `template/` directory removed so there's no duplicated code to maintain.
|
|
93
|
+
|
|
94
|
+
**Acceptance Criteria:**
|
|
95
|
+
- [ ] `template/default/` deleted
|
|
96
|
+
- [ ] `template/webgl/` deleted
|
|
97
|
+
- [ ] `template/webgpu/` deleted
|
|
98
|
+
- [ ] `template/experiment/` deleted
|
|
99
|
+
- [ ] No remaining references to `template/` paths in source code
|
|
100
|
+
- [ ] No remaining references to `basementstudio/basement/template/` in source code
|
|
101
|
+
|
|
102
|
+
### US-008: End-to-end verification
|
|
103
|
+
**Description:** As a CLI maintainer, I need to verify all template + CMS + animation combinations work correctly.
|
|
104
|
+
|
|
105
|
+
**Acceptance Criteria:**
|
|
106
|
+
- [ ] `basement -c test-default -d -no-cms -no-animation -claude -no-hooks` produces a working project
|
|
107
|
+
- [ ] `basement -c test-webgl -webgl -no-cms -no-animation -claude -no-hooks` produces a working project with R3F components
|
|
108
|
+
- [ ] `basement -c test-webgpu -webgpu -no-cms -no-animation -claude -no-hooks` produces a working project with WebGPU deps
|
|
109
|
+
- [ ] `basement -c test-experiment -exp -no-cms -no-animation -claude -no-hooks` produces a working project with experiment header
|
|
110
|
+
- [ ] `basement -c test-sanity -webgl -sanity -gsap -claude -no-hooks` produces a working project with CMS + animation + layer
|
|
111
|
+
- [ ] All generated projects pass `bun install && bun dev` (dev server starts)
|
|
112
|
+
|
|
113
|
+
## Functional Requirements
|
|
114
|
+
|
|
115
|
+
- FR-1: The CLI must clone `basementstudio/next-starter#main` as the base for all template types
|
|
116
|
+
- FR-2: When `type !== 'default'`, the CLI must apply the corresponding layer from the local `layers/` directory
|
|
117
|
+
- FR-3: Layer injection must copy additive files without overwriting existing files
|
|
118
|
+
- FR-4: Layer injection must replace files listed in `replaceFiles`, overwriting the base version
|
|
119
|
+
- FR-5: Layer injection must respect `src/` directory structure (using existing `detectPathPrefix`)
|
|
120
|
+
- FR-6: Layer dependencies must be merged into `package.json` from `LAYER_CONFIG`
|
|
121
|
+
- FR-7: CMS integration must continue to work identically after layer injection
|
|
122
|
+
- FR-8: The `bun.lock` file must be deleted after cloning next-starter
|
|
123
|
+
- FR-9: The user-facing prompt flow must remain unchanged (same 4 template choices)
|
|
124
|
+
- FR-10: The `template/` directory must be deleted from the repository
|
|
125
|
+
|
|
126
|
+
## Non-Goals
|
|
127
|
+
|
|
128
|
+
- No multi-layer support (combining WebGL + Experiment) — one layer per project
|
|
129
|
+
- No `add-layer` command for existing projects (only `create` flow)
|
|
130
|
+
- No version pinning UI — always clones latest `main`
|
|
131
|
+
- No changes to the CMS integration system (`src/mergers/` for Sanity/BaseHub)
|
|
132
|
+
- No changes to the hooks, plugins, or agent skills systems
|
|
133
|
+
- No new merger functions for layers (layers use simple copy/replace, not smart merge)
|
|
134
|
+
|
|
135
|
+
## Technical Considerations
|
|
136
|
+
|
|
137
|
+
- **Reuse existing infrastructure:** `detectPathPrefix()`, `transformPath()`, and `formatMergeResults()` from `src/mergers/index.js` are already built for this pattern
|
|
138
|
+
- **Layer files are local:** Unlike CMS integrations (cloned via tiged from a remote branch), layer files live in the CLI repo's `layers/` directory. No network request needed.
|
|
139
|
+
- **Application order matters:** Layers run before CMS integration. Layers do NOT modify `layout.tsx`, so the Sanity layout merger continues to work on the unmodified base layout.
|
|
140
|
+
- **Package.json format:** `next-starter` uses `package.json` (not `package.template.json`), simplifying hydration logic.
|
|
141
|
+
|
|
142
|
+
### Files to Modify
|
|
143
|
+
|
|
144
|
+
| File | Change Type |
|
|
145
|
+
|------|-------------|
|
|
146
|
+
| `src/mergers/config.js` | Add `LAYER_CONFIG`, `getLayerConfig()` |
|
|
147
|
+
| `src/mergers/index.js` | Add `injectLayer()`, extend `formatMergeResults()` |
|
|
148
|
+
| `src/commands/create.js` | Change tiged source, add layer step, config-driven deps |
|
|
149
|
+
|
|
150
|
+
### Files to Create
|
|
151
|
+
|
|
152
|
+
| File | Source |
|
|
153
|
+
|------|--------|
|
|
154
|
+
| `layers/webgl/app/page.tsx` | `template/webgl/app/page.tsx` |
|
|
155
|
+
| `layers/webgl/components/webgl/canvas/dynamic.tsx` | `template/webgl/components/webgl/canvas/dynamic.tsx` |
|
|
156
|
+
| `layers/webgl/components/webgl/canvas/index.tsx` | `template/webgl/components/webgl/canvas/index.tsx` |
|
|
157
|
+
| `layers/webgl/components/webgl/components/scene/index.tsx` | `template/webgl/components/webgl/components/scene/index.tsx` |
|
|
158
|
+
| `layers/experiment/components/layout/header/index.tsx` | `template/experiment/components/layout/header/index.tsx` |
|
|
159
|
+
| `layers/experiment/components/layout/navigation-menu.tsx` | `template/experiment/components/layout/navigation-menu.tsx` |
|
|
160
|
+
| `layers/experiment/lib/constats.ts` | `template/experiment/lib/constats.ts` |
|
|
161
|
+
| `layers/experiment/lib/utils/cn.ts` | `template/experiment/lib/utils/cn.ts` |
|
|
162
|
+
|
|
163
|
+
### Files/Directories to Delete
|
|
164
|
+
|
|
165
|
+
| Path | Reason |
|
|
166
|
+
|------|--------|
|
|
167
|
+
| `template/default/` | Replaced by next-starter clone |
|
|
168
|
+
| `template/webgl/` | Replaced by `layers/webgl/` |
|
|
169
|
+
| `template/webgpu/` | Replaced by `layers/webgpu/` |
|
|
170
|
+
| `template/experiment/` | Replaced by `layers/experiment/` |
|
|
171
|
+
|
|
172
|
+
## Success Metrics
|
|
173
|
+
|
|
174
|
+
- Template directory eliminated (0 duplicated files to maintain)
|
|
175
|
+
- All 4 template types produce working projects with `bun dev`
|
|
176
|
+
- CMS + layer combinations work correctly
|
|
177
|
+
- CLI prompt flow unchanged (invisible to users)
|
|
178
|
+
- Layer addition requires only: adding files to `layers/`, adding config to `LAYER_CONFIG`
|
|
179
|
+
|
|
180
|
+
## Open Questions
|
|
181
|
+
|
|
182
|
+
- Should we add a `--starter-ref` CLI flag in a future iteration for version pinning?
|
|
183
|
+
- If next-starter changes its directory structure significantly, should the CLI validate expected paths before applying layers?
|
|
184
|
+
- Should the `experiment` template's `constats.ts` typo be fixed to `constants.ts`?
|