create-specra 0.1.4 → 0.1.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-specra",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "CLI tool to scaffold a new Specra documentation site with Next.js",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,80 +1,6 @@
1
- import { NextRequest } from 'next/server'
2
- import { watch } from 'fs'
3
- import { join } from 'path'
1
+ // Next.js requires dynamic to be statically analyzable, so we can't re-export it
2
+ export const dynamic = 'error'
3
+ export const runtime = 'nodejs'
4
4
 
5
- export const dynamic = 'force-static'
6
-
7
- export async function GET(request: NextRequest) {
8
- // Only allow in development mode
9
- if (process.env.NODE_ENV !== 'development') {
10
- return new Response('Not available in production', { status: 404 })
11
- }
12
-
13
- const encoder = new TextEncoder()
14
-
15
- const stream = new ReadableStream({
16
- start(controller) {
17
- // Send initial connection message
18
- const connectMsg = `data: ${JSON.stringify({ type: 'connected' })}\n\n`
19
- controller.enqueue(encoder.encode(connectMsg))
20
-
21
- const docsPath = join(process.cwd(), 'docs')
22
-
23
- // Watch the docs directory recursively
24
- const watcher = watch(
25
- docsPath,
26
- { recursive: true },
27
- (eventType, filename) => {
28
- if (!filename) return
29
-
30
- // Only watch for .mdx and .json files (MDX files and category configs)
31
- if (filename.endsWith('.mdx') || filename.endsWith('.json')) {
32
- console.log(`[MDX Watch] ${eventType}: ${filename}`)
33
-
34
- const message = `data: ${JSON.stringify({
35
- type: 'change',
36
- file: filename,
37
- eventType
38
- })}\n\n`
39
-
40
- try {
41
- controller.enqueue(encoder.encode(message))
42
- } catch (error) {
43
- console.error('[MDX Watch] Error sending message:', error)
44
- }
45
- }
46
- }
47
- )
48
-
49
- // Handle client disconnect
50
- request.signal.addEventListener('abort', () => {
51
- console.log('[MDX Watch] Client disconnected')
52
- watcher.close()
53
- try {
54
- controller.close()
55
- } catch (error) {
56
- // Controller might already be closed
57
- }
58
- })
59
-
60
- // Handle errors
61
- watcher.on('error', (error) => {
62
- console.error('[MDX Watch] Watcher error:', error)
63
- watcher.close()
64
- try {
65
- controller.close()
66
- } catch (e) {
67
- // Controller might already be closed
68
- }
69
- })
70
- }
71
- })
72
-
73
- return new Response(stream, {
74
- headers: {
75
- 'Content-Type': 'text/event-stream',
76
- 'Cache-Control': 'no-cache, no-transform',
77
- 'Connection': 'keep-alive',
78
- },
79
- })
80
- }
5
+ // Re-export the GET handler from SDK
6
+ export { GET } from "specra/app/api/mdx-watch"
@@ -8,7 +8,6 @@ import {
8
8
  getCachedAllDocs,
9
9
  getCachedDocBySlug,
10
10
  getConfig,
11
- SpecraConfig,
12
11
  } from "specra/lib"
13
12
  import {
14
13
  DocLayout,
@@ -23,8 +22,6 @@ import {
23
22
  DocLoading,
24
23
  } from "specra/components"
25
24
 
26
- import specraConfig from "./../../../../specra.config.json"
27
-
28
25
  interface PageProps {
29
26
  params: Promise<{
30
27
  version: string
@@ -35,7 +32,6 @@ interface PageProps {
35
32
  export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
36
33
  const { version, slug: slugArray } = await params
37
34
  const slug = slugArray.join("/")
38
-
39
35
  const doc = await getCachedDocBySlug(slug, version)
40
36
 
41
37
  if (!doc) {
@@ -78,7 +74,6 @@ export async function generateStaticParams() {
78
74
  for (const version of versions) {
79
75
  const docs = await getCachedAllDocs(version)
80
76
  for (const doc of docs) {
81
- // Add the custom slug path
82
77
  params.push({
83
78
  version,
84
79
  slug: doc.slug.split("/").filter(Boolean),
@@ -97,13 +92,9 @@ export default async function DocPage({ params }: PageProps) {
97
92
  const versions = getCachedVersions()
98
93
  const config = getConfig()
99
94
  const isCategory = isCategoryPage(slug, allDocs)
100
-
101
- // Try to get the doc (might be index.mdx or regular .mdx)
102
95
  const doc = await getCachedDocBySlug(slug, version)
103
96
 
104
- // If no doc found and it's a category, show category index
105
97
  if (!doc && isCategory) {
106
- // Find a doc in this category to get the tab group
107
98
  const categoryDoc = allDocs.find((d) => d.slug.startsWith(slug + "/"))
108
99
  const categoryTabGroup = categoryDoc?.meta?.tab_group || categoryDoc?.categoryTabGroup
109
100
 
@@ -113,7 +104,7 @@ export default async function DocPage({ params }: PageProps) {
113
104
  header={<Header currentVersion={version} versions={versions} config={config} />}
114
105
  docs={allDocs}
115
106
  version={version}
116
- content={
107
+ children={
117
108
  <CategoryIndex
118
109
  categoryPath={slug}
119
110
  version={version}
@@ -134,7 +125,6 @@ export default async function DocPage({ params }: PageProps) {
134
125
  )
135
126
  }
136
127
 
137
- // If no doc found, render 404 content within the layout (keeps sidebar visible)
138
128
  if (!doc) {
139
129
  return (
140
130
  <>
@@ -143,7 +133,7 @@ export default async function DocPage({ params }: PageProps) {
143
133
  header={<Header currentVersion={version} versions={versions} config={config} />}
144
134
  docs={allDocs}
145
135
  version={version}
146
- content={<NotFoundContent version={version} />}
136
+ children={<NotFoundContent version={version} />}
147
137
  toc={<div />}
148
138
  config={config}
149
139
  currentPageTabGroup={undefined}
@@ -158,15 +148,7 @@ export default async function DocPage({ params }: PageProps) {
158
148
 
159
149
  const toc = extractTableOfContents(doc.content)
160
150
  const { previous, next } = getAdjacentDocs(slug, allDocs)
161
-
162
- // console.log("[v0] Extracted ToC:", toc)
163
-
164
- // If doc exists but is also a category, show both content and children
165
151
  const showCategoryIndex = isCategory && doc
166
-
167
- // console.log("showCategoryIndex: ", showCategoryIndex)
168
-
169
- // Get current page's tab group from doc metadata or category
170
152
  const currentPageTabGroup = doc.meta?.tab_group || doc.categoryTabGroup
171
153
 
172
154
  return (
@@ -176,36 +158,34 @@ export default async function DocPage({ params }: PageProps) {
176
158
  header={<Header currentVersion={version} versions={versions} config={config} />}
177
159
  docs={allDocs}
178
160
  version={version}
179
- content={
180
- showCategoryIndex ? (
181
- <CategoryIndex
182
- categoryPath={slug}
183
- version={version}
184
- allDocs={allDocs}
185
- title={doc.meta.title}
186
- description={doc.meta.description}
187
- content={doc.content}
188
- config={config}
189
- />
190
- ) : (
191
- <DocLayout
192
- meta={doc.meta}
193
- content={doc.content}
194
- previousDoc={previous ? { title: previous.meta.title, slug: previous.slug } : undefined}
195
- nextDoc={next ? { title: next.meta.title, slug: next.slug } : undefined}
196
- version={version}
197
- slug={slug}
198
- config={config}
199
- />
200
- )
201
- }
202
161
  toc={showCategoryIndex ? <div /> : <TableOfContents items={toc} config={config} />}
203
162
  config={config}
204
163
  currentPageTabGroup={currentPageTabGroup}
205
- />
164
+ >
165
+ {showCategoryIndex ? (
166
+ <CategoryIndex
167
+ categoryPath={slug}
168
+ version={version}
169
+ allDocs={allDocs}
170
+ title={doc.meta.title}
171
+ description={doc.meta.description}
172
+ content={doc.content}
173
+ config={config}
174
+ />
175
+ ) : (
176
+ <DocLayout
177
+ meta={doc.meta}
178
+ content={doc.content}
179
+ previousDoc={previous ? { title: previous.meta.title, slug: previous.slug } : undefined}
180
+ nextDoc={next ? { title: next.meta.title, slug: next.slug } : undefined}
181
+ version={version}
182
+ slug={slug}
183
+ config={config}
184
+ />
185
+ )}
186
+ </DocLayoutWrapper>
206
187
  <MdxHotReload />
207
188
  <HotReloadIndicator />
208
- <DevModeBadge />
209
189
  </Suspense>
210
190
  </>
211
191
  )
@@ -1,33 +1,15 @@
1
- import "server-only"
2
-
3
- import type React from "react"
4
1
  import type { Metadata } from "next"
5
- import { Geist, Geist_Mono } from "next/font/google"
6
-
2
+ import { Geist } from "next/font/google"
7
3
  import { getConfig, getAssetPath, SpecraConfig, initConfig } from "specra/lib"
8
- import { TabProvider, ConfigProvider } from "specra/components"
9
-
4
+ import { ConfigProvider, TabProvider } from "specra/components"
10
5
  import specraConfig from "../specra.config.json"
11
6
  import "./globals.css"
12
7
 
13
- /* -----------------------------
14
- Fonts
15
- ------------------------------ */
16
8
  const geist = Geist({ subsets: ["latin"] })
17
- const geistMono = Geist_Mono({ subsets: ["latin"] })
18
-
19
- // const sans = Geist({ subsets: ["latin"] })
20
- // const geistMono = Geist_Mono({ subsets: ["latin"] })
21
9
 
22
- /* -----------------------------
23
- Initialize Specra config ONCE
24
- (module scope, server-only)
25
- ------------------------------ */
10
+ // Initialize Specra config
26
11
  initConfig(specraConfig as unknown as Partial<SpecraConfig>)
27
12
 
28
- /* -----------------------------
29
- Runtime Metadata (REQUIRED)
30
- ------------------------------ */
31
13
  export async function generateMetadata(): Promise<Metadata> {
32
14
  const config = getConfig()
33
15
 
@@ -37,13 +19,9 @@ export async function generateMetadata(): Promise<Metadata> {
37
19
  template: `%s | ${config.site.title}`,
38
20
  },
39
21
  description: config.site.description || "Modern documentation platform",
40
- metadataBase: config.site.url
41
- ? new URL(config.site.url)
42
- : undefined,
22
+ metadataBase: config.site.url ? new URL(config.site.url) : undefined,
43
23
  icons: {
44
- icon: config.site.favicon
45
- ? [{ url: getAssetPath(config.site.favicon) }]
46
- : [],
24
+ icon: config.site.favicon ? [{ url: getAssetPath(config.site.favicon) }] : [],
47
25
  apple: getAssetPath("/apple-icon.png"),
48
26
  },
49
27
  openGraph: {
@@ -62,24 +40,13 @@ export async function generateMetadata(): Promise<Metadata> {
62
40
  }
63
41
  }
64
42
 
65
- /* -----------------------------
66
- Root Layout
67
- ------------------------------ */
68
- export default function RootLayout({
69
- children,
70
- }: {
71
- children: React.ReactNode
72
- }) {
43
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
73
44
  const config = getConfig()
74
- const defaultTab =
75
- config.navigation?.tabGroups?.[0]?.id ?? ""
45
+ const defaultTab = config.navigation?.tabGroups?.[0]?.id ?? ""
76
46
 
77
47
  return (
78
48
  <html lang={config.site.language || "en"}>
79
- <body
80
- // className={`${geist.className} ${geistMono.className} antialiased`}
81
- className={`font-sans antialiased`}
82
- >
49
+ <body className={`${geist.className} font-sans antialiased`}>
83
50
  <ConfigProvider config={config}>
84
51
  <TabProvider defaultTab={defaultTab}>
85
52
  {children}
@@ -0,0 +1,10 @@
1
+
2
+ import { VersionNotFound } from "specra/components"
3
+
4
+ export default function NotFound() {
5
+ return (
6
+ <>
7
+ <VersionNotFound />
8
+ </>
9
+ )
10
+ }
@@ -1,7 +1,7 @@
1
1
  import Link from "next/link"
2
2
  import { ArrowRight, BookOpen, Zap, Code, Github, Twitter, Linkedin } from "lucide-react"
3
- import { getConfig, getAssetPath } from "specra/lib"
4
- import { Button, SiteBanner } from "specra/components"
3
+ import { getConfig } from "specra/lib"
4
+ import { Button, SiteBanner, Logo } from "specra/components"
5
5
 
6
6
  export default function HomePage() {
7
7
  // Server component - can use getConfig directly
@@ -16,13 +16,7 @@ export default function HomePage() {
16
16
  <header className="border-b border-border">
17
17
  <div className="container flex h-16 items-center justify-between px-6 mx-auto">
18
18
  <Link href="/" className="flex items-center gap-2">
19
- {config.site.logo ? (
20
- <img src={getAssetPath(config.site.logo ?? "")} alt={config.site.title} className="h-8 w-auto" />
21
- ) : (
22
- <div className="h-8 w-8 rounded-xl bg-primary flex items-center justify-center">
23
- <span className="text-primary-foreground font-bold text-lg">S</span>
24
- </div>
25
- )}
19
+ <Logo logo={config.site.logo} alt={config.site.title} className="w-18 object-contain" />
26
20
  <span className="font-semibold text-lg text-foreground">Specra</span>
27
21
  </Link>
28
22
  <div className="flex items-center gap-6">
@@ -1,18 +1 @@
1
- const mode = process.env.NEXT_BUILD_MODE || "default";
2
-
3
- let config;
4
- let target = ""
5
-
6
- switch (mode) {
7
- case "export":
8
- config = await import("./next.config.export.mjs");
9
- target = "export"
10
- break;
11
- default:
12
- config = await import("./next.config.default.mjs");
13
- target = "server"
14
- }
15
-
16
- console.log(`Building for target: ${target}`)
17
-
18
- export default config.default ?? config;
1
+ export { default } from "specra/next-config"
@@ -6408,6 +6408,96 @@
6408
6408
  "type": "github",
6409
6409
  "url": "https://github.com/sponsors/wooorm"
6410
6410
  }
6411
+ },
6412
+ "node_modules/@next/swc-darwin-arm64": {
6413
+ "version": "16.1.0",
6414
+ "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.1.0.tgz",
6415
+ "integrity": "sha512-onHq8dl8KjDb8taANQdzs3XmIqQWV3fYdslkGENuvVInFQzZnuBYYOG2HGHqqtvgmEU7xWzhgndXXxnhk4Z3fQ==",
6416
+ "cpu": [
6417
+ "arm64"
6418
+ ],
6419
+ "optional": true,
6420
+ "os": [
6421
+ "darwin"
6422
+ ],
6423
+ "engines": {
6424
+ "node": ">= 10"
6425
+ }
6426
+ },
6427
+ "node_modules/@next/swc-darwin-x64": {
6428
+ "version": "16.1.0",
6429
+ "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.1.0.tgz",
6430
+ "integrity": "sha512-Am6VJTp8KhLuAH13tPrAoVIXzuComlZlMwGr++o2KDjWiKPe3VwpxYhgV6I4gKls2EnsIMggL4y7GdXyDdJcFA==",
6431
+ "cpu": [
6432
+ "x64"
6433
+ ],
6434
+ "optional": true,
6435
+ "os": [
6436
+ "darwin"
6437
+ ],
6438
+ "engines": {
6439
+ "node": ">= 10"
6440
+ }
6441
+ },
6442
+ "node_modules/@next/swc-linux-arm64-gnu": {
6443
+ "version": "16.1.0",
6444
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.1.0.tgz",
6445
+ "integrity": "sha512-fVicfaJT6QfghNyg8JErZ+EMNQ812IS0lmKfbmC01LF1nFBcKfcs4Q75Yy8IqnsCqH/hZwGhqzj3IGVfWV6vpA==",
6446
+ "cpu": [
6447
+ "arm64"
6448
+ ],
6449
+ "optional": true,
6450
+ "os": [
6451
+ "linux"
6452
+ ],
6453
+ "engines": {
6454
+ "node": ">= 10"
6455
+ }
6456
+ },
6457
+ "node_modules/@next/swc-linux-arm64-musl": {
6458
+ "version": "16.1.0",
6459
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.1.0.tgz",
6460
+ "integrity": "sha512-TojQnDRoX7wJWXEEwdfuJtakMDW64Q7NrxQPviUnfYJvAx5/5wcGE+1vZzQ9F17m+SdpFeeXuOr6v3jbyusYMQ==",
6461
+ "cpu": [
6462
+ "arm64"
6463
+ ],
6464
+ "optional": true,
6465
+ "os": [
6466
+ "linux"
6467
+ ],
6468
+ "engines": {
6469
+ "node": ">= 10"
6470
+ }
6471
+ },
6472
+ "node_modules/@next/swc-win32-arm64-msvc": {
6473
+ "version": "16.1.0",
6474
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.1.0.tgz",
6475
+ "integrity": "sha512-+DK/akkAvvXn5RdYN84IOmLkSy87SCmpofJPdB8vbLmf01BzntPBSYXnMvnEEv/Vcf3HYJwt24QZ/s6sWAwOMQ==",
6476
+ "cpu": [
6477
+ "arm64"
6478
+ ],
6479
+ "optional": true,
6480
+ "os": [
6481
+ "win32"
6482
+ ],
6483
+ "engines": {
6484
+ "node": ">= 10"
6485
+ }
6486
+ },
6487
+ "node_modules/@next/swc-win32-x64-msvc": {
6488
+ "version": "16.1.0",
6489
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.1.0.tgz",
6490
+ "integrity": "sha512-Tr0j94MphimCCks+1rtYPzQFK+faJuhHWCegU9S9gDlgyOk8Y3kPmO64UcjyzZAlligeBtYZ/2bEyrKq0d2wqQ==",
6491
+ "cpu": [
6492
+ "x64"
6493
+ ],
6494
+ "optional": true,
6495
+ "os": [
6496
+ "win32"
6497
+ ],
6498
+ "engines": {
6499
+ "node": ">= 10"
6500
+ }
6411
6501
  }
6412
6502
  }
6413
6503
  }
@@ -18,7 +18,7 @@
18
18
  "next": "^16.1.0",
19
19
  "react": "^19.2.3",
20
20
  "react-dom": "^19.2.3",
21
- "specra": "^0.1.4"
21
+ "specra": "^0.1.6"
22
22
  },
23
23
  "devDependencies": {
24
24
  "@tailwindcss/postcss": "^4.1.9",
@@ -1,13 +1,12 @@
1
- import { NextResponse } from "next/server"
2
- import type { NextRequest } from "next/server"
1
+ import { createSecurityProxy } from "specra/middleware/security"
3
2
 
4
3
  // Note: Redirects from frontmatter are handled at build time via next.config.js
5
- // This middleware can be extended for dynamic redirects if needed
4
+ // This proxy handles runtime security headers, path validation, and dynamic logic
6
5
 
7
- export function middleware(request: NextRequest) {
8
- // Add any runtime middleware logic here
9
- return NextResponse.next()
10
- }
6
+ export const proxy = createSecurityProxy({
7
+ production: process.env.NODE_ENV === "production",
8
+ strictPathValidation: true,
9
+ })
11
10
 
12
11
  export const config = {
13
12
  matcher: [