nextjs-studio 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Tiago Danin
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,127 @@
1
+ # Nextjs Studio
2
+
3
+ A Git-based, local-first CMS for Next.js projects.
4
+
5
+ Content lives in your repository as plain files. No database, no backend, no sync service. Editing happens through a standalone local server. Everything resolves at build time.
6
+
7
+ ## Features
8
+
9
+ - **Content Collections** — folders inside `/contents` become collections automatically
10
+ - **MDX Editor** — rich text editing with TipTap, slash commands, drag & drop blocks, component insertion, and frontmatter binding
11
+ - **JSON Sheet Editor** — spreadsheet-style editing for JSON arrays with inline editing, sorting, and row management
12
+ - **JSON Form Editor** — auto-generated forms for JSON objects with nested field support
13
+ - **Schema Validation** — Zod-based validation with field-level type definitions
14
+ - **Media Library** — per-collection media folders, upload via drag & drop, paste, toolbar, or slash commands
15
+ - **Import Scripts** — run data import and sync scripts directly from the CMS UI
16
+ - **File Watching** — live updates via chokidar and WebSocket
17
+
18
+ ## Quick start
19
+
20
+ ```bash
21
+ yarn add nextjs-studio
22
+ ```
23
+
24
+ ```bash
25
+ npx nextjs-studio
26
+ ```
27
+
28
+ Open [http://localhost:3030](http://localhost:3030).
29
+
30
+ ## Content structure
31
+
32
+ ```
33
+ your-project/
34
+ ├── contents/
35
+ │ ├── blog/
36
+ │ │ ├── hello-world.mdx # MDX collection — one file per entry
37
+ │ │ └── media/ # media assets scoped to this collection
38
+ │ ├── products/
39
+ │ │ └── index.json # JSON array → sheet view in the CMS
40
+ │ └── settings/
41
+ │ └── index.json # JSON object → form view in the CMS
42
+ ├── studio.config.ts # optional — schemas and import scripts
43
+ └── next.config.js
44
+ ```
45
+
46
+ ## Query API
47
+
48
+ ```ts
49
+ import { queryCollection } from "nextjs-studio";
50
+
51
+ // All published posts, newest first
52
+ const posts = queryCollection("blog")
53
+ .where({ published: true })
54
+ .sort("date", "desc")
55
+ .limit(10)
56
+ .all();
57
+
58
+ // Single entry by slug
59
+ const post = queryCollection("blog")
60
+ .where({ slug: "hello-world" })
61
+ .first();
62
+ ```
63
+
64
+ ## Schema definition
65
+
66
+ ```ts
67
+ // studio.config.ts
68
+ import type { StudioConfig } from "nextjs-studio";
69
+
70
+ const config: StudioConfig = {
71
+ collections: {
72
+ blog: {
73
+ schema: {
74
+ collection: "blog",
75
+ label: "Blog Posts",
76
+ fields: [
77
+ { name: "title", type: "text", required: true },
78
+ { name: "slug", type: "slug", from: "title" },
79
+ { name: "published", type: "boolean", defaultValue: false },
80
+ { name: "date", type: "date", includeTime: true },
81
+ { name: "cover", type: "media", accept: ["image/*"] },
82
+ { name: "status", type: "status",
83
+ options: [
84
+ { label: "Draft", value: "draft", color: "gray" },
85
+ { label: "Published", value: "published", color: "green" },
86
+ ],
87
+ },
88
+ ],
89
+ },
90
+ },
91
+ },
92
+ };
93
+
94
+ export default config;
95
+ ```
96
+
97
+ ## Requirements
98
+
99
+ - Node.js >= 22.10.0
100
+ - Next.js 16
101
+
102
+ ## Documentation
103
+
104
+ **Getting Started**
105
+
106
+ - [Introduction](./docs/getting-started/introduction.md)
107
+ - [Installation](./docs/getting-started/installation.md)
108
+ - [Configuration](./docs/getting-started/configuration.md)
109
+
110
+ **Collections**
111
+
112
+ - [Overview](./docs/collections/overview.md)
113
+ - [Media](./docs/collections/media.md)
114
+ - [Import Scripts](./docs/collections/import-scripts.md)
115
+
116
+ **Reference**
117
+
118
+ - [Query API](./docs/reference/query-api.md)
119
+ - [Field Types](./docs/reference/fields.md)
120
+
121
+ **AI**
122
+
123
+ - [Using AI with Nextjs Studio](./docs/ai.md)
124
+
125
+ ## License
126
+
127
+ [MIT](./LICENSE) — Tiago Danin
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
@@ -0,0 +1,189 @@
1
+ #!/usr/bin/env node
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __esm = (fn, res) => function __init() {
5
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
6
+ };
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+
12
+ // package.json
13
+ var package_exports = {};
14
+ __export(package_exports, {
15
+ default: () => package_default
16
+ });
17
+ var package_default;
18
+ var init_package = __esm({
19
+ "package.json"() {
20
+ package_default = {
21
+ name: "nextjs-studio",
22
+ version: "0.1.0",
23
+ description: "A Git-based, local-first CMS for Next.js projects",
24
+ keywords: [
25
+ "nextjs",
26
+ "cms",
27
+ "mdx",
28
+ "content",
29
+ "studio",
30
+ "static-site",
31
+ "local-first"
32
+ ],
33
+ homepage: "https://github.com/TiagoDanin/Nextjs-Studio",
34
+ repository: {
35
+ type: "git",
36
+ url: "https://github.com/TiagoDanin/Nextjs-Studio.git"
37
+ },
38
+ license: "MIT",
39
+ author: "Tiago Danin",
40
+ type: "module",
41
+ exports: {
42
+ ".": {
43
+ types: "./dist/core/index.d.ts",
44
+ import: "./dist/core/index.js"
45
+ }
46
+ },
47
+ main: "./dist/core/index.js",
48
+ types: "./dist/core/index.d.ts",
49
+ bin: "./dist/bin/nextjs-studio.js",
50
+ files: [
51
+ "dist",
52
+ "README.md",
53
+ "LICENSE"
54
+ ],
55
+ scripts: {
56
+ dev: "tsx src/bin/nextjs-studio.ts --dir example/contents",
57
+ "studio:dev": "cross-env STUDIO_CONTENTS_DIR=example/contents next dev --port 3030 --webpack src/cli/ui",
58
+ "studio:build": "next build --webpack src/cli/ui",
59
+ build: "tsup && yarn studio:build",
60
+ lint: "eslint src/",
61
+ "type-check": "tsc --noEmit",
62
+ test: "vitest run",
63
+ "test:watch": "vitest"
64
+ },
65
+ engines: {
66
+ node: ">=22.10.0"
67
+ },
68
+ packageManager: "yarn@4.6.0",
69
+ devDependencies: {
70
+ "@radix-ui/react-collapsible": "^1.1.12",
71
+ "@radix-ui/react-label": "^2.1.8",
72
+ "@radix-ui/react-switch": "^1.2.6",
73
+ "@tailwindcss/postcss": "^4.1.18",
74
+ "@tanstack/react-table": "^8.21.3",
75
+ "@tiptap/extension-bubble-menu": "^3.20.0",
76
+ "@tiptap/extension-code-block-lowlight": "^3.20.0",
77
+ "@tiptap/extension-file-handler": "^3.20.0",
78
+ "@tiptap/extension-image": "^3.20.0",
79
+ "@tiptap/extension-link": "^3.20.0",
80
+ "@tiptap/extension-placeholder": "^3.20.0",
81
+ "@tiptap/react": "^3.20.0",
82
+ "@tiptap/starter-kit": "^3.20.0",
83
+ "@tiptap/suggestion": "^3.20.0",
84
+ "@types/lodash-es": "^4.17.12",
85
+ "@types/node": "^25.2.3",
86
+ "@types/react": "^19",
87
+ "@types/react-dom": "^19",
88
+ "class-variance-authority": "^0.7.1",
89
+ clsx: "^2.1.1",
90
+ "cross-env": "^10.1.0",
91
+ eslint: "^10.0.0",
92
+ lowlight: "^3.3.0",
93
+ "lucide-react": "^0.574.0",
94
+ mermaid: "^11.6.0",
95
+ next: "^16.1.6",
96
+ "next-themes": "^0.4.6",
97
+ react: "^19.2.4",
98
+ "react-dom": "^19.2.4",
99
+ "tailwind-merge": "^3.4.1",
100
+ tailwindcss: "^4.1.18",
101
+ "tippy.js": "^6.3.7",
102
+ "tiptap-extension-global-drag-handle": "^0.1.18",
103
+ "tiptap-markdown": "^0.9.0",
104
+ tsup: "^8.5.1",
105
+ tsx: "^4.21.0",
106
+ typescript: "^5.9.3",
107
+ vitest: "^4.0.18",
108
+ zustand: "^5.0.11"
109
+ },
110
+ dependencies: {
111
+ "@sindresorhus/slugify": "^3.0.0",
112
+ chokidar: "^5.0.0",
113
+ commander: "^14.0.3",
114
+ "gray-matter": "^4.0.3",
115
+ "lodash-es": "^4.17.23"
116
+ }
117
+ };
118
+ }
119
+ });
120
+
121
+ // src/bin/nextjs-studio.ts
122
+ import { existsSync } from "fs";
123
+ import path from "path";
124
+ import { spawn } from "child_process";
125
+ import { Command } from "commander";
126
+
127
+ // src/shared/constants.ts
128
+ var CONTENTS_DIR = "contents";
129
+ var CLI_PORT = 3030;
130
+ var IMAGE_MIME_TYPES = [
131
+ "image/png",
132
+ "image/jpeg",
133
+ "image/gif",
134
+ "image/webp",
135
+ "image/svg+xml",
136
+ "image/avif"
137
+ ];
138
+ var VIDEO_MIME_TYPES = ["video/mp4", "video/webm", "video/ogg"];
139
+ var AUDIO_MIME_TYPES = [
140
+ "audio/mpeg",
141
+ "audio/ogg",
142
+ "audio/wav",
143
+ "audio/webm",
144
+ "audio/aac",
145
+ "audio/flac"
146
+ ];
147
+ var MEDIA_MIME_TYPES = [...IMAGE_MIME_TYPES, ...VIDEO_MIME_TYPES, ...AUDIO_MIME_TYPES];
148
+
149
+ // src/bin/nextjs-studio.ts
150
+ var { version } = await Promise.resolve().then(() => (init_package(), package_exports));
151
+ var program = new Command().name("Nextjs Studio").description("Local-first CMS for Next.js projects").version(version).option("-d, --dir <path>", "Path to contents directory", CONTENTS_DIR).option("-p, --port <number>", "Port to run the studio on", String(CLI_PORT)).parse();
152
+ var opts = program.opts();
153
+ var contentsDir = path.resolve(opts.dir);
154
+ var port = Number(opts.port);
155
+ var uiDir = path.resolve(import.meta.dirname, "../cli/ui");
156
+ var standaloneServer = path.resolve(uiDir, ".next/standalone/src/cli/ui/server.js");
157
+ var serverEnv = {
158
+ ...process.env,
159
+ STUDIO_CONTENTS_DIR: contentsDir,
160
+ PORT: String(port),
161
+ HOSTNAME: "0.0.0.0"
162
+ };
163
+ console.log(`Nextjs Studio v${version}`);
164
+ console.log(`Contents: ${contentsDir}`);
165
+ console.log(`Starting on http://localhost:${port}`);
166
+ function createServerProcess() {
167
+ if (existsSync(standaloneServer)) {
168
+ return spawn("node", [standaloneServer], { stdio: "inherit", env: serverEnv });
169
+ }
170
+ return spawn("npx", ["next", "dev", "--port", String(port), "--webpack"], {
171
+ cwd: uiDir,
172
+ stdio: "inherit",
173
+ shell: true,
174
+ env: serverEnv
175
+ });
176
+ }
177
+ function registerSignalForwarding(child) {
178
+ for (const signal of ["SIGINT", "SIGTERM"]) {
179
+ process.on(signal, () => child.kill(signal));
180
+ }
181
+ }
182
+ var serverProcess = createServerProcess();
183
+ serverProcess.on("error", (error) => {
184
+ console.error("Failed to start server:", error.message);
185
+ process.exit(1);
186
+ });
187
+ serverProcess.on("close", (code) => process.exit(code ?? 0));
188
+ registerSignalForwarding(serverProcess);
189
+ //# sourceMappingURL=nextjs-studio.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../package.json","../../src/bin/nextjs-studio.ts","../../src/shared/constants.ts"],"sourcesContent":["{\n \"name\": \"nextjs-studio\",\n \"version\": \"0.1.0\",\n \"description\": \"A Git-based, local-first CMS for Next.js projects\",\n \"keywords\": [\n \"nextjs\",\n \"cms\",\n \"mdx\",\n \"content\",\n \"studio\",\n \"static-site\",\n \"local-first\"\n ],\n \"homepage\": \"https://github.com/TiagoDanin/Nextjs-Studio\",\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"https://github.com/TiagoDanin/Nextjs-Studio.git\"\n },\n \"license\": \"MIT\",\n \"author\": \"Tiago Danin\",\n \"type\": \"module\",\n \"exports\": {\n \".\": {\n \"types\": \"./dist/core/index.d.ts\",\n \"import\": \"./dist/core/index.js\"\n }\n },\n \"main\": \"./dist/core/index.js\",\n \"types\": \"./dist/core/index.d.ts\",\n \"bin\": \"./dist/bin/nextjs-studio.js\",\n \"files\": [\n \"dist\",\n \"README.md\",\n \"LICENSE\"\n ],\n \"scripts\": {\n \"dev\": \"tsx src/bin/nextjs-studio.ts --dir example/contents\",\n \"studio:dev\": \"cross-env STUDIO_CONTENTS_DIR=example/contents next dev --port 3030 --webpack src/cli/ui\",\n \"studio:build\": \"next build --webpack src/cli/ui\",\n \"build\": \"tsup && yarn studio:build\",\n \"lint\": \"eslint src/\",\n \"type-check\": \"tsc --noEmit\",\n \"test\": \"vitest run\",\n \"test:watch\": \"vitest\"\n },\n \"engines\": {\n \"node\": \">=22.10.0\"\n },\n \"packageManager\": \"yarn@4.6.0\",\n \"devDependencies\": {\n \"@radix-ui/react-collapsible\": \"^1.1.12\",\n \"@radix-ui/react-label\": \"^2.1.8\",\n \"@radix-ui/react-switch\": \"^1.2.6\",\n \"@tailwindcss/postcss\": \"^4.1.18\",\n \"@tanstack/react-table\": \"^8.21.3\",\n \"@tiptap/extension-bubble-menu\": \"^3.20.0\",\n \"@tiptap/extension-code-block-lowlight\": \"^3.20.0\",\n \"@tiptap/extension-file-handler\": \"^3.20.0\",\n \"@tiptap/extension-image\": \"^3.20.0\",\n \"@tiptap/extension-link\": \"^3.20.0\",\n \"@tiptap/extension-placeholder\": \"^3.20.0\",\n \"@tiptap/react\": \"^3.20.0\",\n \"@tiptap/starter-kit\": \"^3.20.0\",\n \"@tiptap/suggestion\": \"^3.20.0\",\n \"@types/lodash-es\": \"^4.17.12\",\n \"@types/node\": \"^25.2.3\",\n \"@types/react\": \"^19\",\n \"@types/react-dom\": \"^19\",\n \"class-variance-authority\": \"^0.7.1\",\n \"clsx\": \"^2.1.1\",\n \"cross-env\": \"^10.1.0\",\n \"eslint\": \"^10.0.0\",\n \"lowlight\": \"^3.3.0\",\n \"lucide-react\": \"^0.574.0\",\n \"mermaid\": \"^11.6.0\",\n \"next\": \"^16.1.6\",\n \"next-themes\": \"^0.4.6\",\n \"react\": \"^19.2.4\",\n \"react-dom\": \"^19.2.4\",\n \"tailwind-merge\": \"^3.4.1\",\n \"tailwindcss\": \"^4.1.18\",\n \"tippy.js\": \"^6.3.7\",\n \"tiptap-extension-global-drag-handle\": \"^0.1.18\",\n \"tiptap-markdown\": \"^0.9.0\",\n \"tsup\": \"^8.5.1\",\n \"tsx\": \"^4.21.0\",\n \"typescript\": \"^5.9.3\",\n \"vitest\": \"^4.0.18\",\n \"zustand\": \"^5.0.11\"\n },\n \"dependencies\": {\n \"@sindresorhus/slugify\": \"^3.0.0\",\n \"chokidar\": \"^5.0.0\",\n \"commander\": \"^14.0.3\",\n \"gray-matter\": \"^4.0.3\",\n \"lodash-es\": \"^4.17.23\"\n }\n}\n","#!/usr/bin/env node\n\n/**\n * @context bin layer — CLI entry point at src/bin/nextjs-studio.ts\n * @does Parses CLI args, resolves paths, and spawns the UI server process\n * @depends src/shared/constants.ts\n * @do Add new CLI flags here; keep only process bootstrap logic\n * @dont Import UI components, access the filesystem beyond existsSync, or contain business logic\n */\n\nimport { existsSync } from \"node:fs\";\nimport path from \"node:path\";\nimport { spawn, type ChildProcess } from \"node:child_process\";\nimport { Command } from \"commander\";\nimport { CLI_PORT, CONTENTS_DIR } from \"../shared/constants.js\";\n\nconst { version } = await import(\"../../package.json\", { with: { type: \"json\" } });\n\nconst program = new Command()\n .name(\"Nextjs Studio\")\n .description(\"Local-first CMS for Next.js projects\")\n .version(version)\n .option(\"-d, --dir <path>\", \"Path to contents directory\", CONTENTS_DIR)\n .option(\"-p, --port <number>\", \"Port to run the studio on\", String(CLI_PORT))\n .parse();\n\nconst opts = program.opts<{ dir: string; port: string }>();\nconst contentsDir = path.resolve(opts.dir);\nconst port = Number(opts.port);\n\nconst uiDir = path.resolve(import.meta.dirname, \"../cli/ui\");\nconst standaloneServer = path.resolve(uiDir, \".next/standalone/src/cli/ui/server.js\");\n\nconst serverEnv = {\n ...process.env,\n STUDIO_CONTENTS_DIR: contentsDir,\n PORT: String(port),\n HOSTNAME: \"0.0.0.0\",\n};\n\nconsole.log(`Nextjs Studio v${version}`);\nconsole.log(`Contents: ${contentsDir}`);\nconsole.log(`Starting on http://localhost:${port}`);\n\nfunction createServerProcess(): ChildProcess {\n if (existsSync(standaloneServer)) {\n return spawn(\"node\", [standaloneServer], { stdio: \"inherit\", env: serverEnv });\n }\n\n return spawn(\"npx\", [\"next\", \"dev\", \"--port\", String(port), \"--webpack\"], {\n cwd: uiDir,\n stdio: \"inherit\",\n shell: true,\n env: serverEnv,\n });\n}\n\nfunction registerSignalForwarding(child: ChildProcess): void {\n for (const signal of [\"SIGINT\", \"SIGTERM\"] as const) {\n process.on(signal, () => child.kill(signal));\n }\n}\n\nconst serverProcess = createServerProcess();\n\nserverProcess.on(\"error\", (error) => {\n console.error(\"Failed to start server:\", error.message);\n process.exit(1);\n});\n\nserverProcess.on(\"close\", (code) => process.exit(code ?? 0));\n\nregisterSignalForwarding(serverProcess);\n","/**\n * @context Shared layer — constants at src/shared/constants.ts\n * @does Defines project-wide constants shared across core, CLI, and UI layers\n * @depends none\n * @do Add new shared constants here\n * @dont Import from CLI or UI; constants must be framework-agnostic\n */\n\nexport const CONTENTS_DIR = \"contents\";\nexport const CLI_PORT = 3030;\nexport const CONFIG_FILE = \"studio.config.ts\";\nexport const SUPPORTED_EXTENSIONS = [\".mdx\", \".json\"] as const;\nexport const COLLECTION_ORDER_FILE = \"collection.json\";\nexport const WATCHER_DEBOUNCE_MS = 5_000;\nexport const MEDIA_DIR = \"media\";\n\nexport const IMAGE_MIME_TYPES = [\n \"image/png\",\n \"image/jpeg\",\n \"image/gif\",\n \"image/webp\",\n \"image/svg+xml\",\n \"image/avif\",\n] as const;\n\nexport const VIDEO_MIME_TYPES = [\"video/mp4\", \"video/webm\", \"video/ogg\"] as const;\n\nexport const AUDIO_MIME_TYPES = [\n \"audio/mpeg\",\n \"audio/ogg\",\n \"audio/wav\",\n \"audio/webm\",\n \"audio/aac\",\n \"audio/flac\",\n] as const;\n\nexport const MEDIA_MIME_TYPES = [...IMAGE_MIME_TYPES, ...VIDEO_MIME_TYPES, ...AUDIO_MIME_TYPES] as const;\n\nexport const IMAGE_EXTENSIONS = [\".png\", \".jpg\", \".jpeg\", \".gif\", \".webp\", \".svg\", \".avif\"] as const;\nexport const VIDEO_EXTENSIONS = [\".mp4\", \".webm\", \".ogv\"] as const;\nexport const AUDIO_EXTENSIONS = [\".mp3\", \".ogg\", \".wav\", \".m4a\", \".aac\", \".flac\"] as const;\n"],"mappings":"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","names":[]}
@@ -0,0 +1,410 @@
1
+ /**
2
+ * @context Shared layer — field type system at src/shared/fields.ts
3
+ * @does Defines every field interface, the FieldDefinition union, and CollectionSchema
4
+ * @depends none
5
+ * @do Add new field types here; the UI and type-generator derive behavior from this shape
6
+ * @dont Import from CLI or UI; contain runtime logic beyond fieldLabel utilities
7
+ */
8
+ declare const __brand: unique symbol;
9
+ type Brand<T, B extends string> = T & {
10
+ readonly [__brand]: B;
11
+ };
12
+ type Email = Brand<string, "Email">;
13
+ type HttpUrl = Brand<string, "HttpUrl">;
14
+ type ISODate = Brand<string, "ISODate">;
15
+ type MediaPath = Brand<string, "MediaPath">;
16
+ type ID = Brand<string, "ID">;
17
+ type Slug = Brand<string, "Slug">;
18
+ interface SelectOption {
19
+ label: string;
20
+ value: string;
21
+ color?: string;
22
+ }
23
+ interface StatusOption {
24
+ label: string;
25
+ value: string;
26
+ /** Semantic color bucket — maps to a badge color in the editor. */
27
+ color?: "gray" | "red" | "yellow" | "green" | "blue" | "purple";
28
+ }
29
+ interface BaseField {
30
+ /** Machine-readable key used in the data object / JSON key. */
31
+ name: string;
32
+ /** Human-readable label shown in the editor. Defaults to `name`. */
33
+ label?: string;
34
+ /** Whether the field must have a non-empty value. */
35
+ required?: boolean;
36
+ /** Helper text shown below the input in the editor. */
37
+ description?: string;
38
+ /** Default value used when creating a new entry. */
39
+ defaultValue?: unknown;
40
+ }
41
+ interface TextField extends BaseField {
42
+ type: "text";
43
+ placeholder?: string;
44
+ maxLength?: number;
45
+ }
46
+ interface LongTextField extends BaseField {
47
+ type: "long-text";
48
+ placeholder?: string;
49
+ /** Minimum visible rows in the textarea. */
50
+ rows?: number;
51
+ }
52
+ interface NumberField extends BaseField {
53
+ type: "number";
54
+ format?: "integer" | "decimal";
55
+ min?: number;
56
+ max?: number;
57
+ step?: number;
58
+ }
59
+ interface BooleanField extends BaseField {
60
+ type: "boolean";
61
+ defaultValue?: boolean;
62
+ }
63
+ interface DateField extends BaseField {
64
+ type: "date";
65
+ /**
66
+ * When `true` the editor renders a datetime-picker (date + time).
67
+ * When `false` or omitted it renders a date-only picker.
68
+ */
69
+ includeTime?: boolean;
70
+ }
71
+ interface SelectField extends BaseField {
72
+ type: "select";
73
+ options: SelectOption[];
74
+ defaultValue?: string;
75
+ }
76
+ interface MultiSelectField extends BaseField {
77
+ type: "multi-select";
78
+ options: SelectOption[];
79
+ defaultValue?: string[];
80
+ }
81
+ interface UrlField extends BaseField {
82
+ type: "url";
83
+ placeholder?: string;
84
+ }
85
+ interface EmailField extends BaseField {
86
+ type: "email";
87
+ placeholder?: string;
88
+ }
89
+ interface MediaField extends BaseField {
90
+ type: "media";
91
+ /** Accepted MIME types or extensions, e.g. `["image/*"]`. */
92
+ accept?: string[];
93
+ }
94
+ interface ObjectField extends BaseField {
95
+ type: "object";
96
+ /** Nested field definitions rendered as a form. */
97
+ fields: FieldDefinition[];
98
+ }
99
+ interface ArrayField extends BaseField {
100
+ type: "array";
101
+ /** Shape of each item in the array — rendered as a sheet (table). */
102
+ itemFields: FieldDefinition[];
103
+ }
104
+ interface IdField extends BaseField {
105
+ type: "id";
106
+ /** Strategy for auto-generating IDs. Defaults to `"nanoid"`. */
107
+ generate?: "uuid" | "nanoid" | "cuid";
108
+ }
109
+ interface SlugField extends BaseField {
110
+ type: "slug";
111
+ /** Name of the field whose value is used as the slug source. */
112
+ from: string;
113
+ }
114
+ interface RelationField extends BaseField {
115
+ type: "relation";
116
+ /** Target collection name. */
117
+ collection: string;
118
+ /** Allow selecting multiple entries. */
119
+ multiple?: boolean;
120
+ }
121
+ interface FormulaField extends BaseField {
122
+ type: "formula";
123
+ /**
124
+ * Expression string evaluated at read time.
125
+ * Syntax TBD — placeholder for v2.
126
+ */
127
+ expression: string;
128
+ /** TypeScript type that the expression produces. Used by the type generator. */
129
+ resultType?: "string" | "number" | "boolean";
130
+ }
131
+ interface StatusField extends BaseField {
132
+ type: "status";
133
+ options: StatusOption[];
134
+ defaultValue?: string;
135
+ }
136
+ interface CreatedTimeField extends BaseField {
137
+ type: "created-time";
138
+ }
139
+ interface UpdatedTimeField extends BaseField {
140
+ type: "updated-time";
141
+ }
142
+ type FieldDefinition = TextField | LongTextField | NumberField | BooleanField | DateField | SelectField | MultiSelectField | UrlField | EmailField | MediaField | ObjectField | ArrayField | IdField | SlugField | RelationField | FormulaField | StatusField | CreatedTimeField | UpdatedTimeField;
143
+ type FieldType = FieldDefinition["type"];
144
+ interface CollectionSchema {
145
+ /** Collection name — must match the folder name under /contents. */
146
+ collection: string;
147
+ /**
148
+ * Human-readable label for the collection.
149
+ * Defaults to the collection name with title case.
150
+ */
151
+ label?: string;
152
+ /** Field definitions that describe the shape of each entry. */
153
+ fields: FieldDefinition[];
154
+ }
155
+
156
+ /**
157
+ * @context Shared layer — domain types at src/shared/types.ts
158
+ * @does Defines core domain interfaces shared across core, CLI, and UI layers
159
+ * @depends src/shared/fields.ts
160
+ * @do Add new shared domain types here
161
+ * @dont Import from CLI or UI; contain runtime logic; import framework-specific code
162
+ */
163
+
164
+ /**
165
+ * Core content entry representing a single piece of content.
166
+ */
167
+ interface ContentEntry {
168
+ /** Collection name derived from the parent folder */
169
+ collection: string;
170
+ /** URL-friendly identifier derived from the filename */
171
+ slug: string;
172
+ /** Full path relative to the contents directory */
173
+ path: string;
174
+ /** Raw body content (MDX string or undefined for JSON) */
175
+ body?: string;
176
+ /** Parsed frontmatter or JSON data */
177
+ data: Record<string, unknown>;
178
+ }
179
+ /**
180
+ * Collection metadata describing a content collection.
181
+ */
182
+ interface Collection {
183
+ /** Collection name (folder name) */
184
+ name: string;
185
+ /** Type of content in the collection */
186
+ type: "mdx" | "json-array" | "json-object";
187
+ /** Number of entries in the collection */
188
+ count: number;
189
+ /** Filesystem path to the collection folder */
190
+ basePath: string;
191
+ /** Optional schema that describes the fields in this collection. */
192
+ schema?: CollectionSchema;
193
+ }
194
+ /**
195
+ * Studio configuration from studio.config.ts
196
+ */
197
+ interface StudioConfig {
198
+ /** Per-collection configuration */
199
+ collections?: Record<string, CollectionConfig>;
200
+ }
201
+ /**
202
+ * Per-collection configuration options.
203
+ */
204
+ interface CollectionConfig {
205
+ /** Field schema that describes the shape of each entry. */
206
+ schema?: CollectionSchema;
207
+ /** Import/sync scripts for the collection */
208
+ scripts?: {
209
+ import?: string;
210
+ sync?: string;
211
+ };
212
+ }
213
+ /**
214
+ * Query options for the content query builder.
215
+ */
216
+ interface QueryOptions {
217
+ where?: Record<string, unknown>;
218
+ sort?: {
219
+ field: string;
220
+ order: "asc" | "desc";
221
+ };
222
+ limit?: number;
223
+ offset?: number;
224
+ }
225
+ /**
226
+ * File metadata returned by the FS adapter.
227
+ */
228
+ interface FileInfo {
229
+ path: string;
230
+ size: number;
231
+ modifiedAt: Date;
232
+ }
233
+ /**
234
+ * A file entry returned by a flat directory listing (non-recursive).
235
+ */
236
+ interface DirectoryFileEntry {
237
+ name: string;
238
+ relativePath: string;
239
+ size: number;
240
+ modifiedAt: Date;
241
+ }
242
+
243
+ /**
244
+ * @context Core layer — query builder at src/core/query-builder.ts
245
+ * @does Provides a fluent API to filter, sort, and paginate content entries from a collection
246
+ * @depends src/shared/types.ts, src/core/content-store.ts
247
+ * @do Add new query capabilities here (e.g. search, groupBy)
248
+ * @dont Import from CLI or UI; access the filesystem; perform I/O
249
+ */
250
+
251
+ /**
252
+ * Fluent query builder for content collections.
253
+ *
254
+ * ```ts
255
+ * const posts = queryCollection("blog")
256
+ * .where({ published: true })
257
+ * .sort("date", "desc")
258
+ * .limit(10)
259
+ * .all();
260
+ * ```
261
+ *
262
+ * Supports dot notation for nested properties:
263
+ * ```ts
264
+ * queryCollection("pages").where({ "hero.title": "Welcome" }).all();
265
+ * ```
266
+ */
267
+ declare class QueryBuilder {
268
+ private readonly collectionName;
269
+ private options;
270
+ constructor(collection: string);
271
+ where(conditions: Record<string, unknown>): this;
272
+ sort(field: string, order?: "asc" | "desc"): this;
273
+ limit(count: number): this;
274
+ offset(count: number): this;
275
+ all(): ContentEntry[];
276
+ first(): ContentEntry | undefined;
277
+ count(): number;
278
+ }
279
+ /**
280
+ * Entry point for querying a content collection.
281
+ */
282
+ declare function queryCollection(collection: string): QueryBuilder;
283
+
284
+ /**
285
+ * @context Shared layer — FS adapter interface at src/shared/fs-adapter.interface.ts
286
+ * @does Defines the IFsAdapter contract so Core can perform I/O without depending on CLI
287
+ * @depends src/shared/types.ts
288
+ * @do Add new I/O methods here when Core needs them; keep the interface minimal
289
+ * @dont Import from CLI or UI; contain implementation logic
290
+ */
291
+
292
+ interface IFsAdapter {
293
+ readFile(filePath: string): Promise<string>;
294
+ writeFile(filePath: string, content: string): Promise<void>;
295
+ deleteFile(filePath: string): Promise<void>;
296
+ exists(filePath: string): Promise<boolean>;
297
+ getStats(filePath: string): Promise<FileInfo>;
298
+ listFiles(dirPath: string, extensions?: readonly string[]): Promise<string[]>;
299
+ listDirectories(dirPath: string): Promise<string[]>;
300
+ readBuffer(filePath: string): Promise<Buffer>;
301
+ writeBuffer(filePath: string, data: Buffer): Promise<void>;
302
+ listAllFiles(dirPath: string): Promise<DirectoryFileEntry[]>;
303
+ join(...segments: string[]): string;
304
+ basename(filePath: string): string;
305
+ extname(filePath: string): string;
306
+ relative(from: string, to: string): string;
307
+ normalizeSlug(relativePath: string, ext: string): string;
308
+ }
309
+
310
+ /**
311
+ * @context Core layer — content indexer at src/core/indexer.ts
312
+ * @does Scans the contents directory, parses MDX/JSON files, and builds an in-memory index
313
+ * @depends src/shared/types.ts, src/shared/constants.ts, src/shared/fs-adapter.interface.ts, src/core/parsers/, src/core/schema-inferrer.ts
314
+ * @do Add new file type handling here; extend indexCollection for new collection behaviors
315
+ * @dont Import from CLI or UI; instantiate FsAdapter; access the filesystem directly
316
+ */
317
+
318
+ declare class ContentIndex {
319
+ private readonly entries;
320
+ private readonly collections;
321
+ private readonly fs;
322
+ constructor(fsAdapter: IFsAdapter);
323
+ build(config?: StudioConfig): Promise<void>;
324
+ getCollection(name: string): ContentEntry[];
325
+ getCollections(): Collection[];
326
+ clear(): void;
327
+ private indexCollection;
328
+ private scanDir;
329
+ private buildMdxEntry;
330
+ private buildJsonEntries;
331
+ private readOrdering;
332
+ private applyOrdering;
333
+ private detectCollectionType;
334
+ }
335
+
336
+ /**
337
+ * @context Core layer — content store at src/core/content-store.ts
338
+ * @does Manages a singleton ContentIndex; exposes loadContent() and getStore() for consumers
339
+ * @depends src/core/indexer.ts, src/shared/fs-adapter.interface.ts, src/shared/types.ts
340
+ * @do Use this as the single access point for in-memory indexed content
341
+ * @dont Import from CLI or UI; instantiate FsAdapter here; contain parsing or I/O logic
342
+ */
343
+
344
+ declare function loadContent(fsAdapter: IFsAdapter, config?: StudioConfig): Promise<ContentIndex>;
345
+
346
+ /**
347
+ * @context Shared layer — schema inference types at src/shared/schema-types.ts
348
+ * @does Provides TypeScript utility types to infer typed data shapes from CollectionSchema
349
+ * @depends src/shared/fields.ts
350
+ * @do Add new schema-level inference utilities here
351
+ * @dont Import from CLI or UI; contain runtime logic or field definitions
352
+ */
353
+
354
+ /** Infer the TypeScript value type for a single field definition. */
355
+ type InferFieldValue<F extends FieldDefinition> = F extends TextField ? string : F extends LongTextField ? string : F extends NumberField ? number : F extends BooleanField ? boolean : F extends DateField ? F["includeTime"] extends true ? Date : ISODate : F extends SelectField ? F["options"][number]["value"] : F extends MultiSelectField ? Array<F["options"][number]["value"]> : F extends UrlField ? HttpUrl : F extends EmailField ? Email : F extends MediaField ? MediaPath : F extends ObjectField ? InferObjectFields<F["fields"]> : F extends ArrayField ? Array<InferObjectFields<F["itemFields"]>> : F extends IdField ? ID : F extends SlugField ? Slug : F extends RelationField ? F["multiple"] extends true ? ID[] : ID : F extends FormulaField ? F["resultType"] extends "number" ? number : F["resultType"] extends "boolean" ? boolean : string : F extends StatusField ? F["options"][number]["value"] : F extends CreatedTimeField ? Date : F extends UpdatedTimeField ? Date : never;
356
+ /**
357
+ * Infer a record type from an array of field definitions.
358
+ * Fields marked `required: false` become optional (`T | undefined`).
359
+ */
360
+ type InferObjectFields<Fields extends FieldDefinition[]> = {
361
+ [F in Fields[number] as F["name"]]: F extends {
362
+ required: false;
363
+ } ? InferFieldValue<F> | undefined : InferFieldValue<F>;
364
+ };
365
+ /**
366
+ * Infer the full data shape of a collection from its schema.
367
+ *
368
+ * @example
369
+ * ```ts
370
+ * const blogSchema = {
371
+ * collection: "blog",
372
+ * fields: [
373
+ * { name: "title", type: "text", required: true },
374
+ * { name: "published", type: "boolean" },
375
+ * ],
376
+ * } satisfies CollectionSchema;
377
+ *
378
+ * type BlogData = InferSchemaData<typeof blogSchema>;
379
+ * // => { title: string; published: boolean }
380
+ * ```
381
+ */
382
+ type InferSchemaData<S extends CollectionSchema> = InferObjectFields<S["fields"]>;
383
+
384
+ /**
385
+ * @context Shared layer — field label utilities at src/shared/field-utils.ts
386
+ * @does Resolves human-readable labels for field definitions and raw key strings
387
+ * @depends src/shared/fields.ts
388
+ * @do Add field-related utility functions here
389
+ * @dont Import from CLI or UI; contain field type definitions or schema logic
390
+ */
391
+
392
+ /**
393
+ * Resolve the human-readable label for a field.
394
+ *
395
+ * When the field definition has an explicit `label`, that is returned as-is.
396
+ * Otherwise the `name` (camelCase / kebab-case / snake_case) is converted to Title Case:
397
+ *
398
+ * @example
399
+ * fieldLabel({ name: "siteName", type: "text" }) // "Site Name"
400
+ * fieldLabel({ name: "created_at", type: "date" }) // "Created At"
401
+ * fieldLabel({ name: "bio", type: "long-text", label: "About" }) // "About"
402
+ */
403
+ declare function fieldLabel(field: Pick<BaseField, "name" | "label">): string;
404
+ /**
405
+ * Resolve the label for a raw key string (no field definition available).
406
+ * Useful for dynamic keys that have no schema entry.
407
+ */
408
+ declare function keyLabel(name: string): string;
409
+
410
+ export { type Collection, type CollectionConfig, type CollectionSchema, type ContentEntry, ContentIndex, type FieldDefinition, type FieldType, type InferFieldValue, type InferSchemaData, type QueryOptions, type StudioConfig, fieldLabel, keyLabel, loadContent, queryCollection };
@@ -0,0 +1,319 @@
1
+ // src/core/query-builder.ts
2
+ import { filter, orderBy, get, slice } from "lodash-es";
3
+
4
+ // src/core/indexer.ts
5
+ import slugify from "@sindresorhus/slugify";
6
+
7
+ // src/shared/constants.ts
8
+ var COLLECTION_ORDER_FILE = "collection.json";
9
+ var IMAGE_MIME_TYPES = [
10
+ "image/png",
11
+ "image/jpeg",
12
+ "image/gif",
13
+ "image/webp",
14
+ "image/svg+xml",
15
+ "image/avif"
16
+ ];
17
+ var VIDEO_MIME_TYPES = ["video/mp4", "video/webm", "video/ogg"];
18
+ var AUDIO_MIME_TYPES = [
19
+ "audio/mpeg",
20
+ "audio/ogg",
21
+ "audio/wav",
22
+ "audio/webm",
23
+ "audio/aac",
24
+ "audio/flac"
25
+ ];
26
+ var MEDIA_MIME_TYPES = [...IMAGE_MIME_TYPES, ...VIDEO_MIME_TYPES, ...AUDIO_MIME_TYPES];
27
+
28
+ // src/core/parsers/parser-mdx.ts
29
+ import matter from "gray-matter";
30
+ function parseMdx(content) {
31
+ const { data, content: body } = matter(content);
32
+ return { data, body: body.trim() };
33
+ }
34
+
35
+ // src/core/parsers/parser-json.ts
36
+ function parseJson(content) {
37
+ const parsed = JSON.parse(content);
38
+ if (Array.isArray(parsed)) {
39
+ return {
40
+ type: "json-array",
41
+ entries: parsed
42
+ };
43
+ }
44
+ if (typeof parsed === "object" && parsed !== null) {
45
+ return {
46
+ type: "json-object",
47
+ data: parsed
48
+ };
49
+ }
50
+ throw new Error("JSON content must be an array or object");
51
+ }
52
+
53
+ // src/core/schema-inferrer.ts
54
+ var RE_ISO_DATE = /^\d{4}-\d{2}-\d{2}$/;
55
+ var RE_ISO_DATETIME = /^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}(:\d{2}(\.\d+)?)?(Z|[+-]\d{2}:?\d{2})?$/;
56
+ var RE_EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
57
+ var RE_URL = /^https?:\/\/.+/;
58
+ var LONG_TEXT_THRESHOLD = 200;
59
+ function isISODate(value) {
60
+ return RE_ISO_DATE.test(value);
61
+ }
62
+ function isISODateTime(value) {
63
+ return RE_ISO_DATETIME.test(value);
64
+ }
65
+ function isEmail(value) {
66
+ return RE_EMAIL.test(value);
67
+ }
68
+ function isUrl(value) {
69
+ return RE_URL.test(value);
70
+ }
71
+ function inferStringField(name, strings) {
72
+ if (strings.every(isEmail)) return { name, type: "email" };
73
+ if (strings.every(isUrl)) return { name, type: "url" };
74
+ if (strings.every(isISODateTime)) return { name, type: "date", includeTime: true };
75
+ if (strings.every(isISODate)) return { name, type: "date" };
76
+ const isLong = strings.some((s) => s.length > LONG_TEXT_THRESHOLD || s.includes("\n"));
77
+ return { name, type: isLong ? "long-text" : "text" };
78
+ }
79
+ function inferArrayField(name, items) {
80
+ if (items.length === 0) return { name, type: "array", itemFields: [] };
81
+ if (items.every((item) => typeof item === "string")) {
82
+ const unique = [...new Set(items)].slice(0, 50);
83
+ const options = unique.map((v) => ({ label: v, value: v }));
84
+ return { name, type: "multi-select", options };
85
+ }
86
+ if (items.every((item) => typeof item === "object" && item !== null && !Array.isArray(item))) {
87
+ return { name, type: "array", itemFields: inferFields(items) };
88
+ }
89
+ return { name, type: "array", itemFields: [] };
90
+ }
91
+ function inferFieldDefinition(name, values) {
92
+ const present = values.filter((v) => v !== null && v !== void 0);
93
+ if (present.length === 0) return { name, type: "text" };
94
+ if (present.every((v) => typeof v === "boolean")) return { name, type: "boolean" };
95
+ if (present.every((v) => typeof v === "number")) {
96
+ const format = present.every((v) => Number.isInteger(v)) ? "integer" : "decimal";
97
+ return { name, type: "number", format };
98
+ }
99
+ if (present.every((v) => typeof v === "string")) {
100
+ return inferStringField(name, present);
101
+ }
102
+ if (present.every((v) => Array.isArray(v))) {
103
+ return inferArrayField(name, present.flat());
104
+ }
105
+ if (present.every((v) => typeof v === "object" && v !== null && !Array.isArray(v))) {
106
+ return { name, type: "object", fields: inferFields(present) };
107
+ }
108
+ return { name, type: "text" };
109
+ }
110
+ function inferFields(rows) {
111
+ const keySet = new Set(rows.flatMap((row) => Object.keys(row)));
112
+ return Array.from(keySet).map((key) => inferFieldDefinition(key, rows.map((row) => row[key])));
113
+ }
114
+ function inferSchema(entries, collectionName) {
115
+ const rows = entries.map((entry) => entry.data);
116
+ return { collection: collectionName, fields: inferFields(rows) };
117
+ }
118
+
119
+ // src/core/indexer.ts
120
+ var ContentIndex = class {
121
+ entries = /* @__PURE__ */ new Map();
122
+ collections = /* @__PURE__ */ new Map();
123
+ fs;
124
+ constructor(fsAdapter) {
125
+ this.fs = fsAdapter;
126
+ }
127
+ async build(config) {
128
+ this.clear();
129
+ const dirs = await this.fs.listDirectories(".");
130
+ for (const dir of dirs) {
131
+ const dirName = this.fs.basename(dir);
132
+ const collectionName = slugify(dirName);
133
+ const collectionConfig = config?.collections?.[collectionName];
134
+ await this.indexCollection(dirName, collectionName, collectionConfig?.schema);
135
+ }
136
+ }
137
+ getCollection(name) {
138
+ return this.entries.get(name) ?? [];
139
+ }
140
+ getCollections() {
141
+ return Array.from(this.collections.values());
142
+ }
143
+ clear() {
144
+ this.entries.clear();
145
+ this.collections.clear();
146
+ }
147
+ async indexCollection(dirName, collectionName, manualSchema) {
148
+ const entries = [];
149
+ await this.scanDir(dirName, collectionName, dirName, entries);
150
+ const orderPath = this.fs.join(dirName, COLLECTION_ORDER_FILE);
151
+ const ordering = await this.readOrdering(orderPath);
152
+ if (ordering) {
153
+ this.applyOrdering(entries, ordering);
154
+ }
155
+ const schema = manualSchema ?? inferSchema(entries, collectionName);
156
+ this.entries.set(collectionName, entries);
157
+ this.collections.set(collectionName, {
158
+ name: collectionName,
159
+ type: this.detectCollectionType(entries),
160
+ count: entries.length,
161
+ basePath: dirName,
162
+ schema
163
+ });
164
+ }
165
+ async scanDir(dirName, collectionName, dirPath, entries) {
166
+ const subDirs = await this.fs.listDirectories(dirPath);
167
+ for (const subDir of subDirs) {
168
+ await this.scanDir(dirName, collectionName, subDir, entries);
169
+ }
170
+ const files = await this.fs.listFiles(dirPath);
171
+ for (const filePath of files) {
172
+ const fileName = this.fs.basename(filePath);
173
+ if (fileName === COLLECTION_ORDER_FILE) continue;
174
+ const ext = this.fs.extname(fileName);
175
+ const content = await this.fs.readFile(filePath);
176
+ const relativePath = this.fs.relative(dirName, filePath);
177
+ const slug = this.fs.normalizeSlug(relativePath, ext).split("/").map((segment) => slugify(segment)).join("/");
178
+ if (ext === ".mdx") {
179
+ entries.push(this.buildMdxEntry(collectionName, slug, content));
180
+ } else if (ext === ".json") {
181
+ entries.push(...this.buildJsonEntries(collectionName, slug, content));
182
+ }
183
+ }
184
+ }
185
+ buildMdxEntry(collectionName, slug, content) {
186
+ const parsed = parseMdx(content);
187
+ return {
188
+ collection: collectionName,
189
+ slug,
190
+ path: `/${collectionName}/${slug}`,
191
+ body: parsed.body,
192
+ data: parsed.data
193
+ };
194
+ }
195
+ buildJsonEntries(collectionName, slug, content) {
196
+ const parsed = parseJson(content);
197
+ if (parsed.type === "json-array") {
198
+ return parsed.entries.map((data, index) => {
199
+ const entrySlug = typeof data["slug"] === "string" ? slugify(data["slug"]) : `${slug}/${index}`;
200
+ return {
201
+ collection: collectionName,
202
+ slug: entrySlug,
203
+ path: `/${collectionName}/${entrySlug}`,
204
+ data
205
+ };
206
+ });
207
+ }
208
+ return [{ collection: collectionName, slug, path: `/${collectionName}/${slug}`, data: parsed.data }];
209
+ }
210
+ async readOrdering(orderPath) {
211
+ if (!await this.fs.exists(orderPath)) return null;
212
+ try {
213
+ const content = await this.fs.readFile(orderPath);
214
+ const parsed = JSON.parse(content);
215
+ if (Array.isArray(parsed)) return parsed;
216
+ } catch (error) {
217
+ console.warn(`[Nextjs Studio] Failed to parse ordering file: ${orderPath}`, error);
218
+ }
219
+ return null;
220
+ }
221
+ applyOrdering(entries, ordering) {
222
+ const orderMap = new Map(ordering.map((slug, index) => [slug, index]));
223
+ entries.sort((a, b) => {
224
+ const aIndex = orderMap.get(a.slug) ?? Infinity;
225
+ const bIndex = orderMap.get(b.slug) ?? Infinity;
226
+ return aIndex - bIndex;
227
+ });
228
+ }
229
+ detectCollectionType(entries) {
230
+ if (entries.length === 0) return "mdx";
231
+ const first = entries[0];
232
+ if (first.body !== void 0) return "mdx";
233
+ if (entries.length === 1 && !first.slug.includes("/")) return "json-object";
234
+ return "json-array";
235
+ }
236
+ };
237
+
238
+ // src/core/content-store.ts
239
+ var store = null;
240
+ function getStore() {
241
+ if (!store) {
242
+ throw new Error("Content not loaded. Call loadContent() before querying.");
243
+ }
244
+ return store;
245
+ }
246
+ async function loadContent(fsAdapter, config) {
247
+ const index = new ContentIndex(fsAdapter);
248
+ await index.build(config);
249
+ store = index;
250
+ return index;
251
+ }
252
+
253
+ // src/core/query-builder.ts
254
+ var QueryBuilder = class {
255
+ collectionName;
256
+ options = {};
257
+ constructor(collection) {
258
+ this.collectionName = collection;
259
+ }
260
+ where(conditions) {
261
+ this.options.where = { ...this.options.where, ...conditions };
262
+ return this;
263
+ }
264
+ sort(field, order = "asc") {
265
+ this.options.sort = { field, order };
266
+ return this;
267
+ }
268
+ limit(count) {
269
+ this.options.limit = count;
270
+ return this;
271
+ }
272
+ offset(count) {
273
+ this.options.offset = count;
274
+ return this;
275
+ }
276
+ all() {
277
+ let entries = [...getStore().getCollection(this.collectionName)];
278
+ if (this.options.where) {
279
+ const conditions = this.options.where;
280
+ entries = filter(
281
+ entries,
282
+ (entry) => Object.entries(conditions).every(([key, value]) => get(entry.data, key) === value)
283
+ );
284
+ }
285
+ if (this.options.sort) {
286
+ const { field, order } = this.options.sort;
287
+ entries = orderBy(entries, [(entry) => get(entry.data, field)], [order]);
288
+ }
289
+ const start = this.options.offset ?? 0;
290
+ const end = this.options.limit ? start + this.options.limit : void 0;
291
+ return slice(entries, start, end);
292
+ }
293
+ first() {
294
+ return this.limit(1).all()[0];
295
+ }
296
+ count() {
297
+ return this.all().length;
298
+ }
299
+ };
300
+ function queryCollection(collection) {
301
+ return new QueryBuilder(collection);
302
+ }
303
+
304
+ // src/shared/field-utils.ts
305
+ function fieldLabel(field) {
306
+ if (field.label) return field.label;
307
+ return field.name.replace(/[-_](.)/g, (_, c) => ` ${c.toUpperCase()}`).replace(/([A-Z])/g, " $1").trim().replace(/\b\w/g, (c) => c.toUpperCase());
308
+ }
309
+ function keyLabel(name) {
310
+ return fieldLabel({ name });
311
+ }
312
+ export {
313
+ ContentIndex,
314
+ fieldLabel,
315
+ keyLabel,
316
+ loadContent,
317
+ queryCollection
318
+ };
319
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/core/query-builder.ts","../../src/core/indexer.ts","../../src/shared/constants.ts","../../src/core/parsers/parser-mdx.ts","../../src/core/parsers/parser-json.ts","../../src/core/schema-inferrer.ts","../../src/core/content-store.ts","../../src/shared/field-utils.ts"],"sourcesContent":["/**\n * @context Core layer — query builder at src/core/query-builder.ts\n * @does Provides a fluent API to filter, sort, and paginate content entries from a collection\n * @depends src/shared/types.ts, src/core/content-store.ts\n * @do Add new query capabilities here (e.g. search, groupBy)\n * @dont Import from CLI or UI; access the filesystem; perform I/O\n */\n\nimport { filter, orderBy, get, slice } from \"lodash-es\";\nimport type { ContentEntry, QueryOptions } from \"../shared/types.js\";\nimport { getStore } from \"./content-store.js\";\n\n/**\n * Fluent query builder for content collections.\n *\n * ```ts\n * const posts = queryCollection(\"blog\")\n * .where({ published: true })\n * .sort(\"date\", \"desc\")\n * .limit(10)\n * .all();\n * ```\n *\n * Supports dot notation for nested properties:\n * ```ts\n * queryCollection(\"pages\").where({ \"hero.title\": \"Welcome\" }).all();\n * ```\n */\nexport class QueryBuilder {\n private readonly collectionName: string;\n private options: QueryOptions = {};\n\n constructor(collection: string) {\n this.collectionName = collection;\n }\n\n where(conditions: Record<string, unknown>): this {\n this.options.where = { ...this.options.where, ...conditions };\n return this;\n }\n\n sort(field: string, order: \"asc\" | \"desc\" = \"asc\"): this {\n this.options.sort = { field, order };\n return this;\n }\n\n limit(count: number): this {\n this.options.limit = count;\n return this;\n }\n\n offset(count: number): this {\n this.options.offset = count;\n return this;\n }\n\n all(): ContentEntry[] {\n let entries = [...getStore().getCollection(this.collectionName)];\n\n if (this.options.where) {\n const conditions = this.options.where;\n entries = filter(entries, (entry) =>\n Object.entries(conditions).every(([key, value]) => get(entry.data, key) === value),\n );\n }\n\n if (this.options.sort) {\n const { field, order } = this.options.sort;\n entries = orderBy(entries, [(entry) => get(entry.data, field)], [order]);\n }\n\n const start = this.options.offset ?? 0;\n const end = this.options.limit ? start + this.options.limit : undefined;\n return slice(entries, start, end);\n }\n\n first(): ContentEntry | undefined {\n return this.limit(1).all()[0];\n }\n\n count(): number {\n return this.all().length;\n }\n}\n\n/**\n * Entry point for querying a content collection.\n */\nexport function queryCollection(collection: string): QueryBuilder {\n return new QueryBuilder(collection);\n}\n","/**\n * @context Core layer — content indexer at src/core/indexer.ts\n * @does Scans the contents directory, parses MDX/JSON files, and builds an in-memory index\n * @depends src/shared/types.ts, src/shared/constants.ts, src/shared/fs-adapter.interface.ts, src/core/parsers/, src/core/schema-inferrer.ts\n * @do Add new file type handling here; extend indexCollection for new collection behaviors\n * @dont Import from CLI or UI; instantiate FsAdapter; access the filesystem directly\n */\n\nimport slugify from \"@sindresorhus/slugify\";\nimport type { CollectionSchema } from \"../shared/fields.js\";\nimport type { ContentEntry, Collection, StudioConfig } from \"../shared/types.js\";\nimport type { IFsAdapter } from \"../shared/fs-adapter.interface.js\";\nimport { COLLECTION_ORDER_FILE } from \"../shared/constants.js\";\nimport { parseMdx } from \"./parsers/parser-mdx.js\";\nimport { parseJson } from \"./parsers/parser-json.js\";\nimport { inferSchema } from \"./schema-inferrer.js\";\n\nexport class ContentIndex {\n private readonly entries = new Map<string, ContentEntry[]>();\n private readonly collections = new Map<string, Collection>();\n private readonly fs: IFsAdapter;\n\n constructor(fsAdapter: IFsAdapter) {\n this.fs = fsAdapter;\n }\n\n async build(config?: StudioConfig): Promise<void> {\n this.clear();\n const dirs = await this.fs.listDirectories(\".\");\n\n for (const dir of dirs) {\n const dirName = this.fs.basename(dir);\n const collectionName = slugify(dirName);\n const collectionConfig = config?.collections?.[collectionName];\n await this.indexCollection(dirName, collectionName, collectionConfig?.schema);\n }\n }\n\n getCollection(name: string): ContentEntry[] {\n return this.entries.get(name) ?? [];\n }\n\n getCollections(): Collection[] {\n return Array.from(this.collections.values());\n }\n\n clear(): void {\n this.entries.clear();\n this.collections.clear();\n }\n\n private async indexCollection(\n dirName: string,\n collectionName: string,\n manualSchema?: CollectionSchema,\n ): Promise<void> {\n const entries: ContentEntry[] = [];\n await this.scanDir(dirName, collectionName, dirName, entries);\n\n const orderPath = this.fs.join(dirName, COLLECTION_ORDER_FILE);\n const ordering = await this.readOrdering(orderPath);\n if (ordering) {\n this.applyOrdering(entries, ordering);\n }\n\n const schema = manualSchema ?? inferSchema(entries, collectionName);\n\n this.entries.set(collectionName, entries);\n this.collections.set(collectionName, {\n name: collectionName,\n type: this.detectCollectionType(entries),\n count: entries.length,\n basePath: dirName,\n schema,\n });\n }\n\n private async scanDir(\n dirName: string,\n collectionName: string,\n dirPath: string,\n entries: ContentEntry[],\n ): Promise<void> {\n const subDirs = await this.fs.listDirectories(dirPath);\n for (const subDir of subDirs) {\n await this.scanDir(dirName, collectionName, subDir, entries);\n }\n\n const files = await this.fs.listFiles(dirPath);\n for (const filePath of files) {\n const fileName = this.fs.basename(filePath);\n if (fileName === COLLECTION_ORDER_FILE) continue;\n\n const ext = this.fs.extname(fileName);\n const content = await this.fs.readFile(filePath);\n const relativePath = this.fs.relative(dirName, filePath);\n const slug = this.fs\n .normalizeSlug(relativePath, ext)\n .split(\"/\")\n .map((segment) => slugify(segment))\n .join(\"/\");\n\n if (ext === \".mdx\") {\n entries.push(this.buildMdxEntry(collectionName, slug, content));\n } else if (ext === \".json\") {\n entries.push(...this.buildJsonEntries(collectionName, slug, content));\n }\n }\n }\n\n private buildMdxEntry(collectionName: string, slug: string, content: string): ContentEntry {\n const parsed = parseMdx(content);\n return {\n collection: collectionName,\n slug,\n path: `/${collectionName}/${slug}`,\n body: parsed.body,\n data: parsed.data,\n };\n }\n\n private buildJsonEntries(collectionName: string, slug: string, content: string): ContentEntry[] {\n const parsed = parseJson(content);\n\n if (parsed.type === \"json-array\") {\n return parsed.entries.map((data, index) => {\n const entrySlug =\n typeof data[\"slug\"] === \"string\" ? slugify(data[\"slug\"]) : `${slug}/${index}`;\n return {\n collection: collectionName,\n slug: entrySlug,\n path: `/${collectionName}/${entrySlug}`,\n data,\n };\n });\n }\n\n return [{ collection: collectionName, slug, path: `/${collectionName}/${slug}`, data: parsed.data }];\n }\n\n private async readOrdering(orderPath: string): Promise<string[] | null> {\n if (!(await this.fs.exists(orderPath))) return null;\n\n try {\n const content = await this.fs.readFile(orderPath);\n const parsed: unknown = JSON.parse(content);\n if (Array.isArray(parsed)) return parsed as string[];\n } catch (error) {\n console.warn(`[Nextjs Studio] Failed to parse ordering file: ${orderPath}`, error);\n }\n return null;\n }\n\n private applyOrdering(entries: ContentEntry[], ordering: string[]): void {\n const orderMap = new Map(ordering.map((slug, index) => [slug, index]));\n entries.sort((a, b) => {\n const aIndex = orderMap.get(a.slug) ?? Infinity;\n const bIndex = orderMap.get(b.slug) ?? Infinity;\n return aIndex - bIndex;\n });\n }\n\n private detectCollectionType(entries: ContentEntry[]): Collection[\"type\"] {\n if (entries.length === 0) return \"mdx\";\n const first = entries[0];\n if (first.body !== undefined) return \"mdx\";\n if (entries.length === 1 && !first.slug.includes(\"/\")) return \"json-object\";\n return \"json-array\";\n }\n}\n","/**\n * @context Shared layer — constants at src/shared/constants.ts\n * @does Defines project-wide constants shared across core, CLI, and UI layers\n * @depends none\n * @do Add new shared constants here\n * @dont Import from CLI or UI; constants must be framework-agnostic\n */\n\nexport const CONTENTS_DIR = \"contents\";\nexport const CLI_PORT = 3030;\nexport const CONFIG_FILE = \"studio.config.ts\";\nexport const SUPPORTED_EXTENSIONS = [\".mdx\", \".json\"] as const;\nexport const COLLECTION_ORDER_FILE = \"collection.json\";\nexport const WATCHER_DEBOUNCE_MS = 5_000;\nexport const MEDIA_DIR = \"media\";\n\nexport const IMAGE_MIME_TYPES = [\n \"image/png\",\n \"image/jpeg\",\n \"image/gif\",\n \"image/webp\",\n \"image/svg+xml\",\n \"image/avif\",\n] as const;\n\nexport const VIDEO_MIME_TYPES = [\"video/mp4\", \"video/webm\", \"video/ogg\"] as const;\n\nexport const AUDIO_MIME_TYPES = [\n \"audio/mpeg\",\n \"audio/ogg\",\n \"audio/wav\",\n \"audio/webm\",\n \"audio/aac\",\n \"audio/flac\",\n] as const;\n\nexport const MEDIA_MIME_TYPES = [...IMAGE_MIME_TYPES, ...VIDEO_MIME_TYPES, ...AUDIO_MIME_TYPES] as const;\n\nexport const IMAGE_EXTENSIONS = [\".png\", \".jpg\", \".jpeg\", \".gif\", \".webp\", \".svg\", \".avif\"] as const;\nexport const VIDEO_EXTENSIONS = [\".mp4\", \".webm\", \".ogv\"] as const;\nexport const AUDIO_EXTENSIONS = [\".mp3\", \".ogg\", \".wav\", \".m4a\", \".aac\", \".flac\"] as const;\n","/**\n * @context Core layer — MDX parser/serializer at src/core/parsers/parser-mdx.ts\n * @does Parses .mdx content into frontmatter + body, and serializes them back to MDX strings\n * @depends none (gray-matter is an external dep)\n * @do Add MDX transform steps here; both parse and serialize live here intentionally\n * @dont Access the filesystem; import from CLI or UI; handle JSON content\n */\n\nimport matter from \"gray-matter\";\n\nexport interface ParsedMdx {\n data: Record<string, unknown>;\n body: string;\n}\n\nexport function parseMdx(content: string): ParsedMdx {\n const { data, content: body } = matter(content);\n return { data, body: body.trim() };\n}\n\nexport function serializeMdx(data: Record<string, unknown>, body: string): string {\n return matter.stringify(body, data);\n}\n","/**\n * @context Core layer — JSON parser at src/core/parsers/parser-json.ts\n * @does Parses JSON content strings into typed ParsedJson results (array or object)\n * @depends none\n * @do Extend ParsedJson variants here if new JSON structures are supported\n * @dont Access the filesystem; import from CLI or UI; contain serialization logic\n */\n\nexport interface ParsedJsonArray {\n type: \"json-array\";\n entries: Record<string, unknown>[];\n}\n\nexport interface ParsedJsonObject {\n type: \"json-object\";\n data: Record<string, unknown>;\n}\n\nexport type ParsedJson = ParsedJsonArray | ParsedJsonObject;\n\nexport function parseJson(content: string): ParsedJson {\n const parsed: unknown = JSON.parse(content);\n\n if (Array.isArray(parsed)) {\n return {\n type: \"json-array\",\n entries: parsed as Record<string, unknown>[],\n };\n }\n\n if (typeof parsed === \"object\" && parsed !== null) {\n return {\n type: \"json-object\",\n data: parsed as Record<string, unknown>,\n };\n }\n\n throw new Error(\"JSON content must be an array or object\");\n}\n","/**\n * @context Core layer — schema inferrer at src/core/schema-inferrer.ts\n * @does Infers a CollectionSchema from actual content entries when no manual schema is defined\n * @depends src/shared/types.ts, src/shared/fields.ts\n * @do Add new type detection heuristics here (e.g. color, phone)\n * @dont Import from CLI or UI; access the filesystem; perform I/O\n */\n\nimport type { ContentEntry } from \"../shared/types.js\";\nimport type { CollectionSchema, FieldDefinition, SelectOption } from \"../shared/fields.js\";\n\n// Value detector patterns\nconst RE_ISO_DATE = /^\\d{4}-\\d{2}-\\d{2}$/;\nconst RE_ISO_DATETIME =\n /^\\d{4}-\\d{2}-\\d{2}[T ]\\d{2}:\\d{2}(:\\d{2}(\\.\\d+)?)?(Z|[+-]\\d{2}:?\\d{2})?$/;\nconst RE_EMAIL = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\nconst RE_URL = /^https?:\\/\\/.+/;\nconst LONG_TEXT_THRESHOLD = 200;\n\nfunction isISODate(value: string): boolean {\n return RE_ISO_DATE.test(value);\n}\n\nfunction isISODateTime(value: string): boolean {\n return RE_ISO_DATETIME.test(value);\n}\n\nfunction isEmail(value: string): boolean {\n return RE_EMAIL.test(value);\n}\n\nfunction isUrl(value: string): boolean {\n return RE_URL.test(value);\n}\n\nfunction inferStringField(name: string, strings: string[]): FieldDefinition {\n if (strings.every(isEmail)) return { name, type: \"email\" };\n if (strings.every(isUrl)) return { name, type: \"url\" };\n if (strings.every(isISODateTime)) return { name, type: \"date\", includeTime: true };\n if (strings.every(isISODate)) return { name, type: \"date\" };\n\n const isLong = strings.some((s) => s.length > LONG_TEXT_THRESHOLD || s.includes(\"\\n\"));\n return { name, type: isLong ? \"long-text\" : \"text\" };\n}\n\nfunction inferArrayField(name: string, items: unknown[]): FieldDefinition {\n if (items.length === 0) return { name, type: \"array\", itemFields: [] };\n\n if (items.every((item) => typeof item === \"string\")) {\n const unique = [...new Set(items as string[])].slice(0, 50);\n const options: SelectOption[] = unique.map((v) => ({ label: v, value: v }));\n return { name, type: \"multi-select\", options };\n }\n\n if (items.every((item) => typeof item === \"object\" && item !== null && !Array.isArray(item))) {\n return { name, type: \"array\", itemFields: inferFields(items as Record<string, unknown>[]) };\n }\n\n return { name, type: \"array\", itemFields: [] };\n}\n\nfunction inferFieldDefinition(name: string, values: unknown[]): FieldDefinition {\n const present = values.filter((v) => v !== null && v !== undefined);\n\n if (present.length === 0) return { name, type: \"text\" };\n if (present.every((v) => typeof v === \"boolean\")) return { name, type: \"boolean\" };\n\n if (present.every((v) => typeof v === \"number\")) {\n const format = present.every((v) => Number.isInteger(v)) ? \"integer\" : \"decimal\";\n return { name, type: \"number\", format };\n }\n\n if (present.every((v) => typeof v === \"string\")) {\n return inferStringField(name, present as string[]);\n }\n\n if (present.every((v) => Array.isArray(v))) {\n return inferArrayField(name, (present as unknown[][]).flat());\n }\n\n if (present.every((v) => typeof v === \"object\" && v !== null && !Array.isArray(v))) {\n return { name, type: \"object\", fields: inferFields(present as Record<string, unknown>[]) };\n }\n\n return { name, type: \"text\" };\n}\n\nfunction inferFields(rows: Record<string, unknown>[]): FieldDefinition[] {\n const keySet = new Set<string>(rows.flatMap((row) => Object.keys(row)));\n return Array.from(keySet).map((key) => inferFieldDefinition(key, rows.map((row) => row[key])));\n}\n\n/**\n * Infer a `CollectionSchema` from the data of a set of content entries.\n *\n * The result is a best-effort approximation — string fields that look like\n * emails, URLs, or ISO dates get the correct semantic type. Everything else\n * falls back to `text`.\n */\nexport function inferSchema(entries: ContentEntry[], collectionName: string): CollectionSchema {\n const rows = entries.map((entry) => entry.data as Record<string, unknown>);\n return { collection: collectionName, fields: inferFields(rows) };\n}\n","/**\n * @context Core layer — content store at src/core/content-store.ts\n * @does Manages a singleton ContentIndex; exposes loadContent() and getStore() for consumers\n * @depends src/core/indexer.ts, src/shared/fs-adapter.interface.ts, src/shared/types.ts\n * @do Use this as the single access point for in-memory indexed content\n * @dont Import from CLI or UI; instantiate FsAdapter here; contain parsing or I/O logic\n */\n\nimport type { IFsAdapter } from \"../shared/fs-adapter.interface.js\";\nimport type { StudioConfig } from \"../shared/types.js\";\nimport { ContentIndex } from \"./indexer.js\";\n\nlet store: ContentIndex | null = null;\n\nexport function getStore(): ContentIndex {\n if (!store) {\n throw new Error(\"Content not loaded. Call loadContent() before querying.\");\n }\n return store;\n}\n\nexport async function loadContent(\n fsAdapter: IFsAdapter,\n config?: StudioConfig,\n): Promise<ContentIndex> {\n const index = new ContentIndex(fsAdapter);\n await index.build(config);\n store = index;\n return index;\n}\n","/**\n * @context Shared layer — field label utilities at src/shared/field-utils.ts\n * @does Resolves human-readable labels for field definitions and raw key strings\n * @depends src/shared/fields.ts\n * @do Add field-related utility functions here\n * @dont Import from CLI or UI; contain field type definitions or schema logic\n */\n\nimport type { BaseField } from \"./fields.js\";\n\n/**\n * Resolve the human-readable label for a field.\n *\n * When the field definition has an explicit `label`, that is returned as-is.\n * Otherwise the `name` (camelCase / kebab-case / snake_case) is converted to Title Case:\n *\n * @example\n * fieldLabel({ name: \"siteName\", type: \"text\" }) // \"Site Name\"\n * fieldLabel({ name: \"created_at\", type: \"date\" }) // \"Created At\"\n * fieldLabel({ name: \"bio\", type: \"long-text\", label: \"About\" }) // \"About\"\n */\nexport function fieldLabel(field: Pick<BaseField, \"name\" | \"label\">): string {\n if (field.label) return field.label;\n return field.name\n .replace(/[-_](.)/g, (_, c: string) => ` ${c.toUpperCase()}`)\n .replace(/([A-Z])/g, \" $1\")\n .trim()\n .replace(/\\b\\w/g, (c) => c.toUpperCase());\n}\n\n/**\n * Resolve the label for a raw key string (no field definition available).\n * Useful for dynamic keys that have no schema entry.\n */\nexport function keyLabel(name: string): string {\n return fieldLabel({ name });\n}\n"],"mappings":";AAQA,SAAS,QAAQ,SAAS,KAAK,aAAa;;;ACA5C,OAAO,aAAa;;;ACIb,IAAM,wBAAwB;AAI9B,IAAM,mBAAmB;AAAA,EAC9B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAEO,IAAM,mBAAmB,CAAC,aAAa,cAAc,WAAW;AAEhE,IAAM,mBAAmB;AAAA,EAC9B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAEO,IAAM,mBAAmB,CAAC,GAAG,kBAAkB,GAAG,kBAAkB,GAAG,gBAAgB;;;AC5B9F,OAAO,YAAY;AAOZ,SAAS,SAAS,SAA4B;AACnD,QAAM,EAAE,MAAM,SAAS,KAAK,IAAI,OAAO,OAAO;AAC9C,SAAO,EAAE,MAAM,MAAM,KAAK,KAAK,EAAE;AACnC;;;ACEO,SAAS,UAAU,SAA6B;AACrD,QAAM,SAAkB,KAAK,MAAM,OAAO;AAE1C,MAAI,MAAM,QAAQ,MAAM,GAAG;AACzB,WAAO;AAAA,MACL,MAAM;AAAA,MACN,SAAS;AAAA,IACX;AAAA,EACF;AAEA,MAAI,OAAO,WAAW,YAAY,WAAW,MAAM;AACjD,WAAO;AAAA,MACL,MAAM;AAAA,MACN,MAAM;AAAA,IACR;AAAA,EACF;AAEA,QAAM,IAAI,MAAM,yCAAyC;AAC3D;;;AC1BA,IAAM,cAAc;AACpB,IAAM,kBACJ;AACF,IAAM,WAAW;AACjB,IAAM,SAAS;AACf,IAAM,sBAAsB;AAE5B,SAAS,UAAU,OAAwB;AACzC,SAAO,YAAY,KAAK,KAAK;AAC/B;AAEA,SAAS,cAAc,OAAwB;AAC7C,SAAO,gBAAgB,KAAK,KAAK;AACnC;AAEA,SAAS,QAAQ,OAAwB;AACvC,SAAO,SAAS,KAAK,KAAK;AAC5B;AAEA,SAAS,MAAM,OAAwB;AACrC,SAAO,OAAO,KAAK,KAAK;AAC1B;AAEA,SAAS,iBAAiB,MAAc,SAAoC;AAC1E,MAAI,QAAQ,MAAM,OAAO,EAAG,QAAO,EAAE,MAAM,MAAM,QAAQ;AACzD,MAAI,QAAQ,MAAM,KAAK,EAAG,QAAO,EAAE,MAAM,MAAM,MAAM;AACrD,MAAI,QAAQ,MAAM,aAAa,EAAG,QAAO,EAAE,MAAM,MAAM,QAAQ,aAAa,KAAK;AACjF,MAAI,QAAQ,MAAM,SAAS,EAAG,QAAO,EAAE,MAAM,MAAM,OAAO;AAE1D,QAAM,SAAS,QAAQ,KAAK,CAAC,MAAM,EAAE,SAAS,uBAAuB,EAAE,SAAS,IAAI,CAAC;AACrF,SAAO,EAAE,MAAM,MAAM,SAAS,cAAc,OAAO;AACrD;AAEA,SAAS,gBAAgB,MAAc,OAAmC;AACxE,MAAI,MAAM,WAAW,EAAG,QAAO,EAAE,MAAM,MAAM,SAAS,YAAY,CAAC,EAAE;AAErE,MAAI,MAAM,MAAM,CAAC,SAAS,OAAO,SAAS,QAAQ,GAAG;AACnD,UAAM,SAAS,CAAC,GAAG,IAAI,IAAI,KAAiB,CAAC,EAAE,MAAM,GAAG,EAAE;AAC1D,UAAM,UAA0B,OAAO,IAAI,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,EAAE,EAAE;AAC1E,WAAO,EAAE,MAAM,MAAM,gBAAgB,QAAQ;AAAA,EAC/C;AAEA,MAAI,MAAM,MAAM,CAAC,SAAS,OAAO,SAAS,YAAY,SAAS,QAAQ,CAAC,MAAM,QAAQ,IAAI,CAAC,GAAG;AAC5F,WAAO,EAAE,MAAM,MAAM,SAAS,YAAY,YAAY,KAAkC,EAAE;AAAA,EAC5F;AAEA,SAAO,EAAE,MAAM,MAAM,SAAS,YAAY,CAAC,EAAE;AAC/C;AAEA,SAAS,qBAAqB,MAAc,QAAoC;AAC9E,QAAM,UAAU,OAAO,OAAO,CAAC,MAAM,MAAM,QAAQ,MAAM,MAAS;AAElE,MAAI,QAAQ,WAAW,EAAG,QAAO,EAAE,MAAM,MAAM,OAAO;AACtD,MAAI,QAAQ,MAAM,CAAC,MAAM,OAAO,MAAM,SAAS,EAAG,QAAO,EAAE,MAAM,MAAM,UAAU;AAEjF,MAAI,QAAQ,MAAM,CAAC,MAAM,OAAO,MAAM,QAAQ,GAAG;AAC/C,UAAM,SAAS,QAAQ,MAAM,CAAC,MAAM,OAAO,UAAU,CAAC,CAAC,IAAI,YAAY;AACvE,WAAO,EAAE,MAAM,MAAM,UAAU,OAAO;AAAA,EACxC;AAEA,MAAI,QAAQ,MAAM,CAAC,MAAM,OAAO,MAAM,QAAQ,GAAG;AAC/C,WAAO,iBAAiB,MAAM,OAAmB;AAAA,EACnD;AAEA,MAAI,QAAQ,MAAM,CAAC,MAAM,MAAM,QAAQ,CAAC,CAAC,GAAG;AAC1C,WAAO,gBAAgB,MAAO,QAAwB,KAAK,CAAC;AAAA,EAC9D;AAEA,MAAI,QAAQ,MAAM,CAAC,MAAM,OAAO,MAAM,YAAY,MAAM,QAAQ,CAAC,MAAM,QAAQ,CAAC,CAAC,GAAG;AAClF,WAAO,EAAE,MAAM,MAAM,UAAU,QAAQ,YAAY,OAAoC,EAAE;AAAA,EAC3F;AAEA,SAAO,EAAE,MAAM,MAAM,OAAO;AAC9B;AAEA,SAAS,YAAY,MAAoD;AACvE,QAAM,SAAS,IAAI,IAAY,KAAK,QAAQ,CAAC,QAAQ,OAAO,KAAK,GAAG,CAAC,CAAC;AACtE,SAAO,MAAM,KAAK,MAAM,EAAE,IAAI,CAAC,QAAQ,qBAAqB,KAAK,KAAK,IAAI,CAAC,QAAQ,IAAI,GAAG,CAAC,CAAC,CAAC;AAC/F;AASO,SAAS,YAAY,SAAyB,gBAA0C;AAC7F,QAAM,OAAO,QAAQ,IAAI,CAAC,UAAU,MAAM,IAA+B;AACzE,SAAO,EAAE,YAAY,gBAAgB,QAAQ,YAAY,IAAI,EAAE;AACjE;;;AJrFO,IAAM,eAAN,MAAmB;AAAA,EACP,UAAU,oBAAI,IAA4B;AAAA,EAC1C,cAAc,oBAAI,IAAwB;AAAA,EAC1C;AAAA,EAEjB,YAAY,WAAuB;AACjC,SAAK,KAAK;AAAA,EACZ;AAAA,EAEA,MAAM,MAAM,QAAsC;AAChD,SAAK,MAAM;AACX,UAAM,OAAO,MAAM,KAAK,GAAG,gBAAgB,GAAG;AAE9C,eAAW,OAAO,MAAM;AACtB,YAAM,UAAU,KAAK,GAAG,SAAS,GAAG;AACpC,YAAM,iBAAiB,QAAQ,OAAO;AACtC,YAAM,mBAAmB,QAAQ,cAAc,cAAc;AAC7D,YAAM,KAAK,gBAAgB,SAAS,gBAAgB,kBAAkB,MAAM;AAAA,IAC9E;AAAA,EACF;AAAA,EAEA,cAAc,MAA8B;AAC1C,WAAO,KAAK,QAAQ,IAAI,IAAI,KAAK,CAAC;AAAA,EACpC;AAAA,EAEA,iBAA+B;AAC7B,WAAO,MAAM,KAAK,KAAK,YAAY,OAAO,CAAC;AAAA,EAC7C;AAAA,EAEA,QAAc;AACZ,SAAK,QAAQ,MAAM;AACnB,SAAK,YAAY,MAAM;AAAA,EACzB;AAAA,EAEA,MAAc,gBACZ,SACA,gBACA,cACe;AACf,UAAM,UAA0B,CAAC;AACjC,UAAM,KAAK,QAAQ,SAAS,gBAAgB,SAAS,OAAO;AAE5D,UAAM,YAAY,KAAK,GAAG,KAAK,SAAS,qBAAqB;AAC7D,UAAM,WAAW,MAAM,KAAK,aAAa,SAAS;AAClD,QAAI,UAAU;AACZ,WAAK,cAAc,SAAS,QAAQ;AAAA,IACtC;AAEA,UAAM,SAAS,gBAAgB,YAAY,SAAS,cAAc;AAElE,SAAK,QAAQ,IAAI,gBAAgB,OAAO;AACxC,SAAK,YAAY,IAAI,gBAAgB;AAAA,MACnC,MAAM;AAAA,MACN,MAAM,KAAK,qBAAqB,OAAO;AAAA,MACvC,OAAO,QAAQ;AAAA,MACf,UAAU;AAAA,MACV;AAAA,IACF,CAAC;AAAA,EACH;AAAA,EAEA,MAAc,QACZ,SACA,gBACA,SACA,SACe;AACf,UAAM,UAAU,MAAM,KAAK,GAAG,gBAAgB,OAAO;AACrD,eAAW,UAAU,SAAS;AAC5B,YAAM,KAAK,QAAQ,SAAS,gBAAgB,QAAQ,OAAO;AAAA,IAC7D;AAEA,UAAM,QAAQ,MAAM,KAAK,GAAG,UAAU,OAAO;AAC7C,eAAW,YAAY,OAAO;AAC5B,YAAM,WAAW,KAAK,GAAG,SAAS,QAAQ;AAC1C,UAAI,aAAa,sBAAuB;AAExC,YAAM,MAAM,KAAK,GAAG,QAAQ,QAAQ;AACpC,YAAM,UAAU,MAAM,KAAK,GAAG,SAAS,QAAQ;AAC/C,YAAM,eAAe,KAAK,GAAG,SAAS,SAAS,QAAQ;AACvD,YAAM,OAAO,KAAK,GACf,cAAc,cAAc,GAAG,EAC/B,MAAM,GAAG,EACT,IAAI,CAAC,YAAY,QAAQ,OAAO,CAAC,EACjC,KAAK,GAAG;AAEX,UAAI,QAAQ,QAAQ;AAClB,gBAAQ,KAAK,KAAK,cAAc,gBAAgB,MAAM,OAAO,CAAC;AAAA,MAChE,WAAW,QAAQ,SAAS;AAC1B,gBAAQ,KAAK,GAAG,KAAK,iBAAiB,gBAAgB,MAAM,OAAO,CAAC;AAAA,MACtE;AAAA,IACF;AAAA,EACF;AAAA,EAEQ,cAAc,gBAAwB,MAAc,SAA+B;AACzF,UAAM,SAAS,SAAS,OAAO;AAC/B,WAAO;AAAA,MACL,YAAY;AAAA,MACZ;AAAA,MACA,MAAM,IAAI,cAAc,IAAI,IAAI;AAAA,MAChC,MAAM,OAAO;AAAA,MACb,MAAM,OAAO;AAAA,IACf;AAAA,EACF;AAAA,EAEQ,iBAAiB,gBAAwB,MAAc,SAAiC;AAC9F,UAAM,SAAS,UAAU,OAAO;AAEhC,QAAI,OAAO,SAAS,cAAc;AAChC,aAAO,OAAO,QAAQ,IAAI,CAAC,MAAM,UAAU;AACzC,cAAM,YACJ,OAAO,KAAK,MAAM,MAAM,WAAW,QAAQ,KAAK,MAAM,CAAC,IAAI,GAAG,IAAI,IAAI,KAAK;AAC7E,eAAO;AAAA,UACL,YAAY;AAAA,UACZ,MAAM;AAAA,UACN,MAAM,IAAI,cAAc,IAAI,SAAS;AAAA,UACrC;AAAA,QACF;AAAA,MACF,CAAC;AAAA,IACH;AAEA,WAAO,CAAC,EAAE,YAAY,gBAAgB,MAAM,MAAM,IAAI,cAAc,IAAI,IAAI,IAAI,MAAM,OAAO,KAAK,CAAC;AAAA,EACrG;AAAA,EAEA,MAAc,aAAa,WAA6C;AACtE,QAAI,CAAE,MAAM,KAAK,GAAG,OAAO,SAAS,EAAI,QAAO;AAE/C,QAAI;AACF,YAAM,UAAU,MAAM,KAAK,GAAG,SAAS,SAAS;AAChD,YAAM,SAAkB,KAAK,MAAM,OAAO;AAC1C,UAAI,MAAM,QAAQ,MAAM,EAAG,QAAO;AAAA,IACpC,SAAS,OAAO;AACd,cAAQ,KAAK,kDAAkD,SAAS,IAAI,KAAK;AAAA,IACnF;AACA,WAAO;AAAA,EACT;AAAA,EAEQ,cAAc,SAAyB,UAA0B;AACvE,UAAM,WAAW,IAAI,IAAI,SAAS,IAAI,CAAC,MAAM,UAAU,CAAC,MAAM,KAAK,CAAC,CAAC;AACrE,YAAQ,KAAK,CAAC,GAAG,MAAM;AACrB,YAAM,SAAS,SAAS,IAAI,EAAE,IAAI,KAAK;AACvC,YAAM,SAAS,SAAS,IAAI,EAAE,IAAI,KAAK;AACvC,aAAO,SAAS;AAAA,IAClB,CAAC;AAAA,EACH;AAAA,EAEQ,qBAAqB,SAA6C;AACxE,QAAI,QAAQ,WAAW,EAAG,QAAO;AACjC,UAAM,QAAQ,QAAQ,CAAC;AACvB,QAAI,MAAM,SAAS,OAAW,QAAO;AACrC,QAAI,QAAQ,WAAW,KAAK,CAAC,MAAM,KAAK,SAAS,GAAG,EAAG,QAAO;AAC9D,WAAO;AAAA,EACT;AACF;;;AK7JA,IAAI,QAA6B;AAE1B,SAAS,WAAyB;AACvC,MAAI,CAAC,OAAO;AACV,UAAM,IAAI,MAAM,yDAAyD;AAAA,EAC3E;AACA,SAAO;AACT;AAEA,eAAsB,YACpB,WACA,QACuB;AACvB,QAAM,QAAQ,IAAI,aAAa,SAAS;AACxC,QAAM,MAAM,MAAM,MAAM;AACxB,UAAQ;AACR,SAAO;AACT;;;ANDO,IAAM,eAAN,MAAmB;AAAA,EACP;AAAA,EACT,UAAwB,CAAC;AAAA,EAEjC,YAAY,YAAoB;AAC9B,SAAK,iBAAiB;AAAA,EACxB;AAAA,EAEA,MAAM,YAA2C;AAC/C,SAAK,QAAQ,QAAQ,EAAE,GAAG,KAAK,QAAQ,OAAO,GAAG,WAAW;AAC5D,WAAO;AAAA,EACT;AAAA,EAEA,KAAK,OAAe,QAAwB,OAAa;AACvD,SAAK,QAAQ,OAAO,EAAE,OAAO,MAAM;AACnC,WAAO;AAAA,EACT;AAAA,EAEA,MAAM,OAAqB;AACzB,SAAK,QAAQ,QAAQ;AACrB,WAAO;AAAA,EACT;AAAA,EAEA,OAAO,OAAqB;AAC1B,SAAK,QAAQ,SAAS;AACtB,WAAO;AAAA,EACT;AAAA,EAEA,MAAsB;AACpB,QAAI,UAAU,CAAC,GAAG,SAAS,EAAE,cAAc,KAAK,cAAc,CAAC;AAE/D,QAAI,KAAK,QAAQ,OAAO;AACtB,YAAM,aAAa,KAAK,QAAQ;AAChC,gBAAU;AAAA,QAAO;AAAA,QAAS,CAAC,UACzB,OAAO,QAAQ,UAAU,EAAE,MAAM,CAAC,CAAC,KAAK,KAAK,MAAM,IAAI,MAAM,MAAM,GAAG,MAAM,KAAK;AAAA,MACnF;AAAA,IACF;AAEA,QAAI,KAAK,QAAQ,MAAM;AACrB,YAAM,EAAE,OAAO,MAAM,IAAI,KAAK,QAAQ;AACtC,gBAAU,QAAQ,SAAS,CAAC,CAAC,UAAU,IAAI,MAAM,MAAM,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC;AAAA,IACzE;AAEA,UAAM,QAAQ,KAAK,QAAQ,UAAU;AACrC,UAAM,MAAM,KAAK,QAAQ,QAAQ,QAAQ,KAAK,QAAQ,QAAQ;AAC9D,WAAO,MAAM,SAAS,OAAO,GAAG;AAAA,EAClC;AAAA,EAEA,QAAkC;AAChC,WAAO,KAAK,MAAM,CAAC,EAAE,IAAI,EAAE,CAAC;AAAA,EAC9B;AAAA,EAEA,QAAgB;AACd,WAAO,KAAK,IAAI,EAAE;AAAA,EACpB;AACF;AAKO,SAAS,gBAAgB,YAAkC;AAChE,SAAO,IAAI,aAAa,UAAU;AACpC;;;AOrEO,SAAS,WAAW,OAAkD;AAC3E,MAAI,MAAM,MAAO,QAAO,MAAM;AAC9B,SAAO,MAAM,KACV,QAAQ,YAAY,CAAC,GAAG,MAAc,IAAI,EAAE,YAAY,CAAC,EAAE,EAC3D,QAAQ,YAAY,KAAK,EACzB,KAAK,EACL,QAAQ,SAAS,CAAC,MAAM,EAAE,YAAY,CAAC;AAC5C;AAMO,SAAS,SAAS,MAAsB;AAC7C,SAAO,WAAW,EAAE,KAAK,CAAC;AAC5B;","names":[]}
package/package.json ADDED
@@ -0,0 +1,100 @@
1
+ {
2
+ "name": "nextjs-studio",
3
+ "version": "0.1.0",
4
+ "description": "A Git-based, local-first CMS for Next.js projects",
5
+ "keywords": [
6
+ "nextjs",
7
+ "cms",
8
+ "mdx",
9
+ "content",
10
+ "studio",
11
+ "static-site",
12
+ "local-first"
13
+ ],
14
+ "homepage": "https://github.com/TiagoDanin/Nextjs-Studio",
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "https://github.com/TiagoDanin/Nextjs-Studio.git"
18
+ },
19
+ "license": "MIT",
20
+ "author": "Tiago Danin",
21
+ "type": "module",
22
+ "exports": {
23
+ ".": {
24
+ "types": "./dist/core/index.d.ts",
25
+ "import": "./dist/core/index.js"
26
+ }
27
+ },
28
+ "main": "./dist/core/index.js",
29
+ "types": "./dist/core/index.d.ts",
30
+ "bin": {
31
+ "nextjs-studio": "./dist/bin/nextjs-studio.js"
32
+ },
33
+ "files": [
34
+ "dist",
35
+ "README.md",
36
+ "LICENSE"
37
+ ],
38
+ "scripts": {
39
+ "dev": "tsx src/bin/nextjs-studio.ts --dir example/contents",
40
+ "studio:dev": "cross-env STUDIO_CONTENTS_DIR=example/contents next dev --port 3030 --webpack src/cli/ui",
41
+ "studio:build": "next build --webpack src/cli/ui",
42
+ "build": "tsup && yarn studio:build",
43
+ "lint": "eslint src/",
44
+ "type-check": "tsc --noEmit",
45
+ "test": "vitest run",
46
+ "test:watch": "vitest"
47
+ },
48
+ "engines": {
49
+ "node": ">=22.10.0"
50
+ },
51
+ "packageManager": "yarn@4.6.0",
52
+ "devDependencies": {
53
+ "@radix-ui/react-collapsible": "^1.1.12",
54
+ "@radix-ui/react-label": "^2.1.8",
55
+ "@radix-ui/react-switch": "^1.2.6",
56
+ "@tailwindcss/postcss": "^4.1.18",
57
+ "@tanstack/react-table": "^8.21.3",
58
+ "@tiptap/extension-bubble-menu": "^3.20.0",
59
+ "@tiptap/extension-code-block-lowlight": "^3.20.0",
60
+ "@tiptap/extension-file-handler": "^3.20.0",
61
+ "@tiptap/extension-image": "^3.20.0",
62
+ "@tiptap/extension-link": "^3.20.0",
63
+ "@tiptap/extension-placeholder": "^3.20.0",
64
+ "@tiptap/react": "^3.20.0",
65
+ "@tiptap/starter-kit": "^3.20.0",
66
+ "@tiptap/suggestion": "^3.20.0",
67
+ "@types/lodash-es": "^4.17.12",
68
+ "@types/node": "^25.2.3",
69
+ "@types/react": "^19",
70
+ "@types/react-dom": "^19",
71
+ "class-variance-authority": "^0.7.1",
72
+ "clsx": "^2.1.1",
73
+ "cross-env": "^10.1.0",
74
+ "eslint": "^10.0.0",
75
+ "lowlight": "^3.3.0",
76
+ "lucide-react": "^0.574.0",
77
+ "mermaid": "^11.6.0",
78
+ "next": "^16.1.6",
79
+ "next-themes": "^0.4.6",
80
+ "react": "^19.2.4",
81
+ "react-dom": "^19.2.4",
82
+ "tailwind-merge": "^3.4.1",
83
+ "tailwindcss": "^4.1.18",
84
+ "tippy.js": "^6.3.7",
85
+ "tiptap-extension-global-drag-handle": "^0.1.18",
86
+ "tiptap-markdown": "^0.9.0",
87
+ "tsup": "^8.5.1",
88
+ "tsx": "^4.21.0",
89
+ "typescript": "^5.9.3",
90
+ "vitest": "^4.0.18",
91
+ "zustand": "^5.0.11"
92
+ },
93
+ "dependencies": {
94
+ "@sindresorhus/slugify": "^3.0.0",
95
+ "chokidar": "^5.0.0",
96
+ "commander": "^14.0.3",
97
+ "gray-matter": "^4.0.3",
98
+ "lodash-es": "^4.17.23"
99
+ }
100
+ }