@jay-framework/data-files 0.23.1

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.
@@ -0,0 +1,211 @@
1
+ # Data Files Plugin
2
+
3
+ Render structured data from CSV, YAML, JSON, or JSONL files as lists, item pages, or single items.
4
+
5
+ ## When to Use
6
+
7
+ | Use case | Plugin |
8
+ | ------------------------------------------------------------- | ----------------------------- |
9
+ | Small/medium static datasets (team, FAQ, features, changelog) | **data-files** |
10
+ | Large, frequently updated catalogs (products, blog posts) | CMS plugin (e.g., wix-stores) |
11
+ | Content-heavy pages (documentation, articles) | markdown plugin |
12
+
13
+ Data files require a **full route rebuild** when data changes. CMS plugins support single-item invalidation. Choose data-files when your data is small, changes infrequently, and doesn't need an API.
14
+
15
+ ## End-to-End Workflow
16
+
17
+ 1. **Place data** — put CSV/YAML/JSON/JSONL files in a content directory
18
+ 2. **Define schema** — create a `.jay-contract` schema file (or run `jay-stack run data-files/generate-schema`)
19
+ 3. **Generate agent-kit** — run `jay-stack agent-kit` to materialize component contracts
20
+ 4. **Use in templates** — import the plugin with the materialized contract name
21
+ 5. **Build** — data is read at render time, cross-references resolved
22
+
23
+ ```
24
+ project/
25
+ ├── content/
26
+ │ ├── team/
27
+ │ │ ├── data.csv # the data
28
+ │ │ └── team.jay-contract # the schema
29
+ │ └── faq/
30
+ │ ├── data.yaml
31
+ │ └── faq.jay-contract
32
+ ├── src/pages/
33
+ │ ├── team/
34
+ │ │ ├── page.jay-html # list view
35
+ │ │ └── [slug]/
36
+ │ │ └── page.jay-html # per-item page
37
+ │ └── faq/
38
+ │ └── page.jay-html # FAQ list
39
+ ```
40
+
41
+ ## Data Formats
42
+
43
+ | Extension | Format | Notes |
44
+ | ---------------- | --------------------- | ------------------------------------------------------------------ |
45
+ | `.csv` | CSV with header row | All values are strings |
46
+ | `.yaml` / `.yml` | YAML array of objects | Types inferred (string, number, boolean); nested objects supported |
47
+ | `.json` | JSON array of objects | Types inferred; nested objects supported |
48
+ | `.jsonl` | JSON Lines | One object per line |
49
+
50
+ ## Schema Contract
51
+
52
+ Every data directory requires a `.jay-contract` schema file. This is the standard contract format — the same syntax used for page and component contracts.
53
+
54
+ ```yaml
55
+ # content/team/team.jay-contract
56
+ name: team
57
+ description: Team members
58
+ tags:
59
+ - tag: slug
60
+ type: data
61
+ dataType: string
62
+ meta:
63
+ slug: 'true' # marks the slug field for routing
64
+ - tag: name
65
+ type: data
66
+ dataType: string
67
+ - tag: role
68
+ type: data
69
+ dataType: string
70
+ - tag: bio
71
+ type: data
72
+ dataType: html-string # rendered HTML content
73
+ ```
74
+
75
+ **Required:** one tag must have `meta.slug: "true"` — this is the item identifier for routing and lookup.
76
+
77
+ If no schema exists, validation emits an error with instructions to create one.
78
+
79
+ ## Three Components
80
+
81
+ ### `data-pages` — per-item pages
82
+
83
+ Each row becomes a page. Use with `[slug]` dynamic routes.
84
+
85
+ ```html
86
+ <head>
87
+ <script
88
+ type="application/jay-headless"
89
+ plugin="@jay-framework/data-files"
90
+ contract="team-data-pages"
91
+ key="member"
92
+ >
93
+ contentDir: content/team
94
+ file: data.csv
95
+ </script>
96
+ </head>
97
+ <body>
98
+ <h1>{member.name}</h1>
99
+ <p>{member.role}</p>
100
+ <div>{member.bio}</div>
101
+ </body>
102
+ ```
103
+
104
+ ### `data-list` — all items as a list
105
+
106
+ ```html
107
+ <head>
108
+ <script
109
+ type="application/jay-headless"
110
+ plugin="@jay-framework/data-files"
111
+ contract="team-data-list"
112
+ key="team"
113
+ >
114
+ contentDir: content/team
115
+ file: data.csv
116
+ </script>
117
+ </head>
118
+ <body>
119
+ <ul>
120
+ <li forEach="team.items" trackBy="slug">
121
+ <a href="/team/{slug}">{name} — {role}</a>
122
+ </li>
123
+ </ul>
124
+ </body>
125
+ ```
126
+
127
+ ### `data-item` — single item by slug
128
+
129
+ ```html
130
+ <jay:team-data-item slug="jane" contentDir="content/team" file="data.csv">
131
+ <div class="card">
132
+ <h3>{name}</h3>
133
+ <p>{role}</p>
134
+ </div>
135
+ </jay:team-data-item>
136
+ ```
137
+
138
+ ## Contract Names
139
+
140
+ The materialized contract names combine the schema name with the component type:
141
+
142
+ | Schema name | Component | Contract name |
143
+ | ----------- | ---------- | ----------------- |
144
+ | `team` | data-pages | `team-data-pages` |
145
+ | `team` | data-list | `team-data-list` |
146
+ | `team` | data-item | `team-data-item` |
147
+
148
+ **Important:** use the materialized contract names (e.g., `team-data-pages`), not the schema contract file directly.
149
+
150
+ ## Cross-References
151
+
152
+ Link to items in other data files using contract `link`:
153
+
154
+ ```yaml
155
+ # content/recipes/recipes.jay-contract
156
+ tags:
157
+ - tag: author
158
+ type: sub-contract
159
+ link: ../team/team.jay-contract
160
+ ```
161
+
162
+ In the data file, the field value is the slug of the referenced item:
163
+
164
+ ```yaml
165
+ - slug: carbonara
166
+ title: Pasta Carbonara
167
+ author: jane # resolved from team data by slug
168
+ ```
169
+
170
+ The plugin resolves the reference at render time — the designer gets `{author.name}` directly.
171
+
172
+ **Inline vs reference:** if the data value is a string, it's a slug reference (resolved from the linked data file). If it's an object, it's inline data (used directly).
173
+
174
+ ## Nested Objects
175
+
176
+ Use inline sub-contracts for nested data:
177
+
178
+ ```yaml
179
+ # Schema
180
+ - tag: nutrition
181
+ type: sub-contract
182
+ tags:
183
+ - tag: calories
184
+ type: data
185
+ dataType: number
186
+
187
+ # Data
188
+ - slug: carbonara
189
+ nutrition:
190
+ calories: 450
191
+ ```
192
+
193
+ ## Limitations
194
+
195
+ - **Size limit:** data files are for datasets under 10,000 rows. Larger files emit a warning — use a CMS plugin instead.
196
+ - **Full rebuild:** changing any row triggers a rebuild of all pages using that file. CMS plugins support single-item updates.
197
+ - **No circular references:** A referencing B referencing A is detected and rejected.
198
+ - **Slug only:** only slug-based routing is supported. Filtering, sorting, and categories require a CMS plugin.
199
+
200
+ ## Pre-Build Script Pattern
201
+
202
+ For data sourced from external systems, use a pre-build script:
203
+
204
+ ```bash
205
+ #!/bin/bash
206
+ # scripts/fetch-data.sh
207
+ curl -s https://api.example.com/team | jq '.' > content/team/data.json
208
+ curl -s https://api.example.com/faq | jq '.' > content/faq/data.json
209
+ ```
210
+
211
+ Run before `jay-stack agent-kit` and `jay-stack build`. The data files are local snapshots — the plugin reads them at build time without network access.
@@ -0,0 +1 @@
1
+
@@ -0,0 +1,133 @@
1
+ import * as _jay_framework_fullstack_component from '@jay-framework/fullstack-component';
2
+ import * as _jay_framework_component from '@jay-framework/component';
3
+
4
+ interface DataPagesProps {
5
+ contentDir: string;
6
+ file: string;
7
+ slug: string;
8
+ }
9
+ declare const dataPages: _jay_framework_fullstack_component.JayStackComponentDefinition<any, any, any, any, [{}], [], DataPagesProps & {
10
+ slug: string;
11
+ }, {
12
+ slug: string;
13
+ }, _jay_framework_component.JayComponentCore<DataPagesProps & {
14
+ slug: string;
15
+ }, any>> & {
16
+ withFastRender<NewCarryForward extends object>(fastRender: _jay_framework_fullstack_component.RenderFast<[{}], DataPagesProps & {
17
+ slug: string;
18
+ }, any, NewCarryForward>): _jay_framework_fullstack_component.JayStackComponentDefinition<any, any, any, any, [{}], [_jay_framework_fullstack_component.Signals<any>, NewCarryForward], DataPagesProps & {
19
+ slug: string;
20
+ }, {
21
+ slug: string;
22
+ }, _jay_framework_component.JayComponentCore<DataPagesProps & {
23
+ slug: string;
24
+ }, any>> & {
25
+ withClientDefaults(fn: (props: DataPagesProps & {
26
+ slug: string;
27
+ }) => {
28
+ viewState: any;
29
+ carryForward?: any;
30
+ }): _jay_framework_fullstack_component.JayStackComponentDefinition<any, any, any, any, [{}], [_jay_framework_fullstack_component.Signals<any>, NewCarryForward], DataPagesProps & {
31
+ slug: string;
32
+ }, {
33
+ slug: string;
34
+ }, _jay_framework_component.JayComponentCore<DataPagesProps & {
35
+ slug: string;
36
+ }, any>> & /*elided*/ any;
37
+ withInteractive(comp: _jay_framework_component.ComponentConstructor<DataPagesProps & {
38
+ slug: string;
39
+ }, any, any, [_jay_framework_fullstack_component.Signals<any>, NewCarryForward], _jay_framework_component.JayComponentCore<DataPagesProps & {
40
+ slug: string;
41
+ }, any>>): _jay_framework_fullstack_component.JayStackComponentDefinition<any, any, any, any, [{}], [_jay_framework_fullstack_component.Signals<any>, NewCarryForward], DataPagesProps & {
42
+ slug: string;
43
+ }, {
44
+ slug: string;
45
+ }, _jay_framework_component.JayComponentCore<DataPagesProps & {
46
+ slug: string;
47
+ }, any>>;
48
+ };
49
+ withInteractive(comp: _jay_framework_component.ComponentConstructor<DataPagesProps & {
50
+ slug: string;
51
+ }, any, any, [], _jay_framework_component.JayComponentCore<DataPagesProps & {
52
+ slug: string;
53
+ }, any>>): _jay_framework_fullstack_component.JayStackComponentDefinition<any, any, any, any, [{}], [], DataPagesProps & {
54
+ slug: string;
55
+ }, {
56
+ slug: string;
57
+ }, _jay_framework_component.JayComponentCore<DataPagesProps & {
58
+ slug: string;
59
+ }, any>>;
60
+ };
61
+
62
+ interface DataListProps {
63
+ contentDir: string;
64
+ file: string;
65
+ }
66
+ declare const dataList: _jay_framework_fullstack_component.JayStackComponentDefinition<any, any, any, any, [{}], [], DataListProps, {}, _jay_framework_component.JayComponentCore<DataListProps, any>> & {
67
+ withFastRender<NewCarryForward extends object>(fastRender: _jay_framework_fullstack_component.RenderFast<[{}], DataListProps, any, NewCarryForward>): _jay_framework_fullstack_component.JayStackComponentDefinition<any, any, any, any, [{}], [_jay_framework_fullstack_component.Signals<any>, NewCarryForward], DataListProps, {}, _jay_framework_component.JayComponentCore<DataListProps, any>> & {
68
+ withClientDefaults(fn: (props: DataListProps) => {
69
+ viewState: any;
70
+ carryForward?: any;
71
+ }): _jay_framework_fullstack_component.JayStackComponentDefinition<any, any, any, any, [{}], [_jay_framework_fullstack_component.Signals<any>, NewCarryForward], DataListProps, {}, _jay_framework_component.JayComponentCore<DataListProps, any>> & /*elided*/ any;
72
+ withInteractive(comp: _jay_framework_component.ComponentConstructor<DataListProps, any, any, [_jay_framework_fullstack_component.Signals<any>, NewCarryForward], _jay_framework_component.JayComponentCore<DataListProps, any>>): _jay_framework_fullstack_component.JayStackComponentDefinition<any, any, any, any, [{}], [_jay_framework_fullstack_component.Signals<any>, NewCarryForward], DataListProps, {}, _jay_framework_component.JayComponentCore<DataListProps, any>>;
73
+ };
74
+ withInteractive(comp: _jay_framework_component.ComponentConstructor<DataListProps, any, any, [], _jay_framework_component.JayComponentCore<DataListProps, any>>): _jay_framework_fullstack_component.JayStackComponentDefinition<any, any, any, any, [{}], [], DataListProps, {}, _jay_framework_component.JayComponentCore<DataListProps, any>>;
75
+ };
76
+
77
+ interface DataItemProps {
78
+ contentDir: string;
79
+ file: string;
80
+ slug: string;
81
+ }
82
+ declare const dataItem: _jay_framework_fullstack_component.JayStackComponentDefinition<any, any, any, any, [{}], [], DataItemProps, {}, _jay_framework_component.JayComponentCore<DataItemProps, any>> & {
83
+ withFastRender<NewCarryForward extends object>(fastRender: _jay_framework_fullstack_component.RenderFast<[{}], DataItemProps, any, NewCarryForward>): _jay_framework_fullstack_component.JayStackComponentDefinition<any, any, any, any, [{}], [_jay_framework_fullstack_component.Signals<any>, NewCarryForward], DataItemProps, {}, _jay_framework_component.JayComponentCore<DataItemProps, any>> & {
84
+ withClientDefaults(fn: (props: DataItemProps) => {
85
+ viewState: any;
86
+ carryForward?: any;
87
+ }): _jay_framework_fullstack_component.JayStackComponentDefinition<any, any, any, any, [{}], [_jay_framework_fullstack_component.Signals<any>, NewCarryForward], DataItemProps, {}, _jay_framework_component.JayComponentCore<DataItemProps, any>> & /*elided*/ any;
88
+ withInteractive(comp: _jay_framework_component.ComponentConstructor<DataItemProps, any, any, [_jay_framework_fullstack_component.Signals<any>, NewCarryForward], _jay_framework_component.JayComponentCore<DataItemProps, any>>): _jay_framework_fullstack_component.JayStackComponentDefinition<any, any, any, any, [{}], [_jay_framework_fullstack_component.Signals<any>, NewCarryForward], DataItemProps, {}, _jay_framework_component.JayComponentCore<DataItemProps, any>>;
89
+ };
90
+ withInteractive(comp: _jay_framework_component.ComponentConstructor<DataItemProps, any, any, [], _jay_framework_component.JayComponentCore<DataItemProps, any>>): _jay_framework_fullstack_component.JayStackComponentDefinition<any, any, any, any, [{}], [], DataItemProps, {}, _jay_framework_component.JayComponentCore<DataItemProps, any>>;
91
+ };
92
+
93
+ type DataRow = Record<string, unknown>;
94
+ declare function parseDataFile(filePath: string): Promise<DataRow[]>;
95
+ declare function clearParseCache(): void;
96
+ declare function buildSlugIndex(rows: DataRow[], slugField: string): Map<string, DataRow>;
97
+
98
+ interface SchemaTag {
99
+ tag: string;
100
+ type: string | string[];
101
+ dataType?: string;
102
+ meta?: Record<string, string>;
103
+ tags?: SchemaTag[];
104
+ link?: string;
105
+ repeated?: boolean;
106
+ trackBy?: string;
107
+ phase?: string;
108
+ description?: string | string[];
109
+ }
110
+ interface DataSchema {
111
+ name: string;
112
+ description?: string;
113
+ tags: SchemaTag[];
114
+ slugField: string;
115
+ }
116
+ declare function loadSchema(contentDir: string): Promise<DataSchema>;
117
+ declare function clearSchemaCache(): void;
118
+
119
+ declare function resolveReferences(row: DataRow, tags: SchemaTag[], contentDir: string, visited?: Set<string>, depth?: number): Promise<Record<string, unknown>>;
120
+ declare function clearFileCache(): void;
121
+
122
+ interface GeneratedContract {
123
+ name: string;
124
+ yaml: string;
125
+ }
126
+ declare function generateDataPagesContract(): AsyncGenerator<GeneratedContract>;
127
+ declare function generateDataListContract(): AsyncGenerator<GeneratedContract>;
128
+ declare function generateDataItemContract(): AsyncGenerator<GeneratedContract>;
129
+
130
+ declare function generateSchema(contentDir: string): Promise<string>;
131
+ declare function generateSchemaCommand(args: string[]): Promise<void>;
132
+
133
+ export { buildSlugIndex, clearFileCache, clearParseCache, clearSchemaCache, dataItem, dataList, dataPages, generateDataItemContract, generateDataListContract, generateDataPagesContract, generateSchema, generateSchemaCommand, loadSchema, parseDataFile, resolveReferences };
package/dist/index.js ADDED
@@ -0,0 +1,546 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
3
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
4
+ import { makeJayStackComponent, notFound, phaseOutput } from "@jay-framework/fullstack-component";
5
+ import fs from "node:fs/promises";
6
+ import path from "node:path";
7
+ import yaml from "js-yaml";
8
+ const CACHE_TTL_MS = 1e4;
9
+ class FileCache {
10
+ constructor() {
11
+ __publicField(this, "entries", /* @__PURE__ */ new Map());
12
+ }
13
+ async get(filePath, loader) {
14
+ const cached = this.entries.get(filePath);
15
+ if (cached) {
16
+ const now = Date.now();
17
+ if (now - cached.checkedAt < CACHE_TTL_MS) return cached.value;
18
+ try {
19
+ const stat2 = await fs.stat(filePath);
20
+ cached.checkedAt = now;
21
+ if (stat2.mtimeMs === cached.mtime) return cached.value;
22
+ } catch {
23
+ }
24
+ }
25
+ const value = await loader();
26
+ const stat = await fs.stat(filePath);
27
+ this.entries.set(filePath, { value, mtime: stat.mtimeMs, checkedAt: Date.now() });
28
+ return value;
29
+ }
30
+ clear() {
31
+ this.entries.clear();
32
+ }
33
+ }
34
+ const cache$1 = new FileCache();
35
+ async function parseDataFile(filePath) {
36
+ const ext = path.extname(filePath).toLowerCase();
37
+ const supportedExtensions = [".csv", ".yaml", ".yml", ".json", ".jsonl"];
38
+ if (!supportedExtensions.includes(ext)) {
39
+ throw new Error(
40
+ `Unsupported data file format "${ext}". Supported: .csv, .yaml, .yml, .json, .jsonl`
41
+ );
42
+ }
43
+ return cache$1.get(filePath, async () => {
44
+ const content = await fs.readFile(filePath, "utf-8");
45
+ switch (ext) {
46
+ case ".csv":
47
+ return parseCsv(content);
48
+ case ".yaml":
49
+ case ".yml":
50
+ return parseYaml(content);
51
+ case ".json":
52
+ return parseJson(content);
53
+ case ".jsonl":
54
+ return parseJsonl(content);
55
+ default:
56
+ throw new Error("Unreachable");
57
+ }
58
+ });
59
+ }
60
+ function parseCsv(content) {
61
+ const lines = content.split("\n").filter((l) => l.trim().length > 0);
62
+ if (lines.length === 0) return [];
63
+ const headers = lines[0].split(",").map((h) => h.trim());
64
+ return lines.slice(1).map((line) => {
65
+ const values = line.split(",").map((v) => v.trim());
66
+ const row = {};
67
+ for (let i = 0; i < headers.length; i++) {
68
+ row[headers[i]] = values[i] ?? "";
69
+ }
70
+ return row;
71
+ });
72
+ }
73
+ function parseYaml(content) {
74
+ const data = yaml.load(content);
75
+ if (!Array.isArray(data)) {
76
+ throw new Error("YAML data file must be an array of objects");
77
+ }
78
+ return data;
79
+ }
80
+ function parseJson(content) {
81
+ const data = JSON.parse(content);
82
+ if (!Array.isArray(data)) {
83
+ throw new Error("JSON data file must be an array of objects");
84
+ }
85
+ return data;
86
+ }
87
+ function parseJsonl(content) {
88
+ return content.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).map((line) => JSON.parse(line));
89
+ }
90
+ function clearParseCache() {
91
+ cache$1.clear();
92
+ }
93
+ function buildSlugIndex(rows, slugField) {
94
+ const index = /* @__PURE__ */ new Map();
95
+ for (const row of rows) {
96
+ const slug = String(row[slugField] ?? "");
97
+ if (slug) {
98
+ index.set(slug, row);
99
+ }
100
+ }
101
+ return index;
102
+ }
103
+ const MAX_ROWS = 1e4;
104
+ const cache = new FileCache();
105
+ async function loadSchema(contentDir) {
106
+ const files = await fs.readdir(contentDir);
107
+ const schemaFile = files.find((f) => f.endsWith(".jay-contract"));
108
+ if (!schemaFile) {
109
+ throw new Error(
110
+ `Data file in "${contentDir}" has no schema contract.
111
+ Create a .jay-contract file to define the data shape.
112
+ Run \`jay-stack run data-files/generate-schema\` to auto-generate from the data file.
113
+ See: agent-kit/designer/data-files-usage.md`
114
+ );
115
+ }
116
+ const filePath = path.join(contentDir, schemaFile);
117
+ return cache.get(filePath, async () => {
118
+ const content = await fs.readFile(filePath, "utf-8");
119
+ const raw = yaml.load(content);
120
+ if (!raw || !raw.name) {
121
+ throw new Error(`Schema contract "${schemaFile}" must have a "name" field`);
122
+ }
123
+ const tags = raw.tags ?? [];
124
+ const slugField = findSlugField(tags);
125
+ if (!slugField) {
126
+ throw new Error(
127
+ `Schema contract "${schemaFile}" has no slug field.
128
+ Mark one tag with meta.slug: "true" to identify the item key.
129
+ See: agent-kit/designer/data-files-usage.md`
130
+ );
131
+ }
132
+ return {
133
+ name: raw.name,
134
+ description: raw.description,
135
+ tags,
136
+ slugField
137
+ };
138
+ });
139
+ }
140
+ function clearSchemaCache() {
141
+ cache.clear();
142
+ }
143
+ function findSlugField(tags) {
144
+ for (const tag of tags) {
145
+ if (tag.meta?.slug === "true") {
146
+ return tag.tag;
147
+ }
148
+ }
149
+ return void 0;
150
+ }
151
+ function validateRowCount(rows, filePath) {
152
+ if (rows.length > MAX_ROWS) {
153
+ return `"${filePath}" has ${rows.length} rows.
154
+ Data files are designed for small/medium datasets (< ${MAX_ROWS} rows).
155
+ For large catalogs, use a CMS plugin with API-based pagination instead.
156
+ See: agent-kit/designer/data-files-usage.md`;
157
+ }
158
+ return void 0;
159
+ }
160
+ const MAX_DEPTH = 10;
161
+ const fileCache = /* @__PURE__ */ new Map();
162
+ async function getSlugIndex(contentDir, file, slugField) {
163
+ const key = `${contentDir}/${file}`;
164
+ if (fileCache.has(key)) return fileCache.get(key);
165
+ const rows = await parseDataFile(path.join(contentDir, file));
166
+ const index = buildSlugIndex(rows, slugField);
167
+ fileCache.set(key, index);
168
+ return index;
169
+ }
170
+ function findDataFile(dir, files) {
171
+ return files.find(
172
+ (f) => f.endsWith(".csv") || f.endsWith(".yaml") || f.endsWith(".yml") || f.endsWith(".json") || f.endsWith(".jsonl")
173
+ );
174
+ }
175
+ async function resolveReferences(row, tags, contentDir, visited = /* @__PURE__ */ new Set(), depth = 0) {
176
+ if (depth > MAX_DEPTH) {
177
+ throw new Error(`Reference resolution exceeded maximum depth (${MAX_DEPTH})`);
178
+ }
179
+ const result = {};
180
+ for (const tag of tags) {
181
+ const value = row[tag.tag];
182
+ if (tag.link && typeof value === "string" && value !== "") {
183
+ const linkPath = path.resolve(contentDir, path.dirname(tag.link));
184
+ const linkedSchema = await loadSchema(linkPath);
185
+ const fs2 = await import("node:fs/promises");
186
+ const dirFiles = await fs2.readdir(linkPath);
187
+ const dataFile = findDataFile(linkPath, dirFiles);
188
+ if (!dataFile) {
189
+ result[tag.tag] = null;
190
+ continue;
191
+ }
192
+ const visitKey = `${linkPath}/${dataFile}:${value}`;
193
+ if (visited.has(visitKey)) {
194
+ throw new Error(
195
+ `Circular reference detected: ${visitKey}
196
+ Circular references are not supported in data files. Remove one of the references.`
197
+ );
198
+ }
199
+ visited.add(visitKey);
200
+ const index = await getSlugIndex(linkPath, dataFile, linkedSchema.slugField);
201
+ const referenced = index.get(value);
202
+ if (referenced) {
203
+ result[tag.tag] = await resolveReferences(
204
+ referenced,
205
+ linkedSchema.tags,
206
+ linkPath,
207
+ visited,
208
+ depth + 1
209
+ );
210
+ } else {
211
+ result[tag.tag] = null;
212
+ }
213
+ } else if (tag.tags && typeof value === "object" && value !== null && !Array.isArray(value)) {
214
+ result[tag.tag] = await resolveReferences(
215
+ value,
216
+ tag.tags,
217
+ contentDir,
218
+ visited,
219
+ depth + 1
220
+ );
221
+ } else if (tag.repeated && Array.isArray(value)) {
222
+ const items = [];
223
+ for (const item of value) {
224
+ if (typeof item === "object" && item !== null && tag.tags) {
225
+ items.push(
226
+ await resolveReferences(
227
+ item,
228
+ tag.tags,
229
+ contentDir,
230
+ visited,
231
+ depth + 1
232
+ )
233
+ );
234
+ } else {
235
+ items.push(item);
236
+ }
237
+ }
238
+ result[tag.tag] = items;
239
+ } else {
240
+ result[tag.tag] = value;
241
+ }
242
+ }
243
+ return result;
244
+ }
245
+ function clearFileCache() {
246
+ fileCache.clear();
247
+ }
248
+ const dataPages = makeJayStackComponent().withProps().withLoadParams(async function* (_services, props) {
249
+ const dir = props?.contentDir;
250
+ const file = props?.file;
251
+ if (!dir || !file) return;
252
+ const schema = await loadSchema(dir);
253
+ const rows = await parseDataFile(path.join(dir, file));
254
+ const slugField = schema.slugField;
255
+ const slugs = rows.map((row) => ({ slug: String(row[slugField] ?? "") })).filter((p) => p.slug !== "");
256
+ yield slugs;
257
+ }).withSlowlyRender(async (props) => {
258
+ const schema = await loadSchema(props.contentDir);
259
+ const filePath = path.join(props.contentDir, props.file);
260
+ const rows = await parseDataFile(filePath);
261
+ const warning = validateRowCount(rows, filePath);
262
+ if (warning) console.warn(warning);
263
+ const index = buildSlugIndex(rows, schema.slugField);
264
+ const row = index.get(props.slug);
265
+ if (!row) return notFound();
266
+ const resolved = await resolveReferences(row, schema.tags, props.contentDir);
267
+ return phaseOutput(resolved, {});
268
+ });
269
+ const dataList = makeJayStackComponent().withProps().withSlowlyRender(async (props) => {
270
+ const schema = await loadSchema(props.contentDir);
271
+ const filePath = path.join(props.contentDir, props.file);
272
+ const rows = await parseDataFile(filePath);
273
+ const warning = validateRowCount(rows, filePath);
274
+ if (warning) console.warn(warning);
275
+ const items = [];
276
+ for (const row of rows) {
277
+ const resolved = await resolveReferences(row, schema.tags, props.contentDir);
278
+ items.push(resolved);
279
+ }
280
+ return phaseOutput({ items }, {});
281
+ });
282
+ const dataItem = makeJayStackComponent().withProps().withSlowlyRender(async (props) => {
283
+ const schema = await loadSchema(props.contentDir);
284
+ const filePath = path.join(props.contentDir, props.file);
285
+ const rows = await parseDataFile(filePath);
286
+ const warning = validateRowCount(rows, filePath);
287
+ if (warning) console.warn(warning);
288
+ const index = buildSlugIndex(rows, schema.slugField);
289
+ const row = index.get(props.slug);
290
+ if (!row) return notFound();
291
+ const resolved = await resolveReferences(row, schema.tags, props.contentDir);
292
+ return phaseOutput(resolved, {});
293
+ });
294
+ function tagsToYaml(tags, indent = 0) {
295
+ const pad = " ".repeat(indent);
296
+ const lines = [];
297
+ for (const tag of tags) {
298
+ lines.push(`${pad}- tag: ${tag.tag}`);
299
+ const typeVal = Array.isArray(tag.type) ? tag.type[0] : tag.type;
300
+ lines.push(`${pad} type: ${typeVal}`);
301
+ if (tag.dataType) lines.push(`${pad} dataType: ${tag.dataType}`);
302
+ if (tag.phase) lines.push(`${pad} phase: ${tag.phase}`);
303
+ if (tag.repeated) lines.push(`${pad} repeated: true`);
304
+ if (tag.trackBy) lines.push(`${pad} trackBy: ${tag.trackBy}`);
305
+ if (tag.link) lines.push(`${pad} link: ${tag.link}`);
306
+ if (tag.description) {
307
+ const desc = Array.isArray(tag.description) ? tag.description[0] : tag.description;
308
+ lines.push(`${pad} description: ${desc}`);
309
+ }
310
+ if (tag.tags && tag.tags.length > 0) {
311
+ lines.push(`${pad} tags:`);
312
+ lines.push(tagsToYaml(tag.tags, indent + 2));
313
+ }
314
+ }
315
+ return lines.join("\n");
316
+ }
317
+ async function findContentDirs(projectRoot) {
318
+ const contentDir = path.join(projectRoot, "content");
319
+ try {
320
+ const entries = await fs.readdir(contentDir, { withFileTypes: true });
321
+ const dirs = [];
322
+ for (const entry of entries) {
323
+ if (!entry.isDirectory()) continue;
324
+ const dirPath = path.join(contentDir, entry.name);
325
+ const files = await fs.readdir(dirPath);
326
+ const hasSchema = files.some((f) => f.endsWith(".jay-contract"));
327
+ if (hasSchema) dirs.push(dirPath);
328
+ }
329
+ return dirs;
330
+ } catch {
331
+ return [];
332
+ }
333
+ }
334
+ async function* generateDataPagesContract() {
335
+ const projectRoot = process.cwd();
336
+ const dirs = await findContentDirs(projectRoot);
337
+ for (const dir of dirs) {
338
+ const schema = await loadSchema(dir);
339
+ const contractYaml = [
340
+ `name: ${schema.name}-data-pages`,
341
+ `description: Per-item pages for ${schema.name}`,
342
+ "",
343
+ "props:",
344
+ " - name: contentDir",
345
+ " kind: required",
346
+ " - name: file",
347
+ " kind: required",
348
+ "",
349
+ "params:",
350
+ " - name: slug",
351
+ " kind: required",
352
+ "",
353
+ "tags:",
354
+ tagsToYaml(schema.tags, 1)
355
+ ].join("\n");
356
+ yield { name: schema.name, yaml: contractYaml };
357
+ }
358
+ }
359
+ async function* generateDataListContract() {
360
+ const projectRoot = process.cwd();
361
+ const dirs = await findContentDirs(projectRoot);
362
+ for (const dir of dirs) {
363
+ const schema = await loadSchema(dir);
364
+ const slugField = schema.slugField;
365
+ const contractYaml = [
366
+ `name: ${schema.name}-data-list`,
367
+ `description: List view for ${schema.name}`,
368
+ "",
369
+ "props:",
370
+ " - name: contentDir",
371
+ " kind: required",
372
+ " - name: file",
373
+ " kind: required",
374
+ "",
375
+ "tags:",
376
+ " - tag: items",
377
+ " type: sub-contract",
378
+ " repeated: true",
379
+ ` trackBy: ${slugField}`,
380
+ " phase: slow",
381
+ " tags:",
382
+ tagsToYaml(schema.tags, 3)
383
+ ].join("\n");
384
+ yield { name: schema.name, yaml: contractYaml };
385
+ }
386
+ }
387
+ async function* generateDataItemContract() {
388
+ const projectRoot = process.cwd();
389
+ const dirs = await findContentDirs(projectRoot);
390
+ for (const dir of dirs) {
391
+ const schema = await loadSchema(dir);
392
+ const contractYaml = [
393
+ `name: ${schema.name}-data-item`,
394
+ `description: Single item view for ${schema.name}`,
395
+ "",
396
+ "props:",
397
+ " - name: contentDir",
398
+ " kind: required",
399
+ " - name: file",
400
+ " kind: required",
401
+ " - name: slug",
402
+ " kind: required",
403
+ "",
404
+ "tags:",
405
+ tagsToYaml(schema.tags, 1)
406
+ ].join("\n");
407
+ yield { name: schema.name, yaml: contractYaml };
408
+ }
409
+ }
410
+ function inferType(value) {
411
+ if (value === null || value === void 0) return "string";
412
+ if (typeof value === "number") return "number";
413
+ if (typeof value === "boolean") return "boolean";
414
+ if (typeof value === "string") {
415
+ if (value.startsWith("<")) return "html-string";
416
+ return "string";
417
+ }
418
+ return "string";
419
+ }
420
+ function inferTags(rows) {
421
+ if (rows.length === 0) return [];
422
+ const firstRow = rows[0];
423
+ const tags = [];
424
+ for (const [key, value] of Object.entries(firstRow)) {
425
+ if (Array.isArray(value)) {
426
+ const itemTags = value.length > 0 && typeof value[0] === "object" && value[0] !== null ? inferTags(value) : [];
427
+ const trackBy = itemTags.find((t) => t.tag === "id" || t.tag === "slug")?.tag;
428
+ tags.push({
429
+ tag: key,
430
+ type: "sub-contract",
431
+ repeated: true,
432
+ trackBy: trackBy || (itemTags.length > 0 ? itemTags[0].tag : "id"),
433
+ tags: itemTags
434
+ });
435
+ } else if (typeof value === "object" && value !== null) {
436
+ tags.push({
437
+ tag: key,
438
+ type: "sub-contract",
439
+ tags: inferTags([value])
440
+ });
441
+ } else {
442
+ tags.push({
443
+ tag: key,
444
+ type: "data",
445
+ dataType: inferType(value)
446
+ });
447
+ }
448
+ }
449
+ return tags;
450
+ }
451
+ function pickSlugField(tags) {
452
+ const candidates = ["slug", "id", "key", "name"];
453
+ for (const candidate of candidates) {
454
+ if (tags.find((t) => t.tag === candidate && t.type === "data")) {
455
+ return candidate;
456
+ }
457
+ }
458
+ return tags.find((t) => t.type === "data")?.tag;
459
+ }
460
+ function tagToYaml(tag, indent) {
461
+ const pad = " ".repeat(indent);
462
+ const lines = [];
463
+ lines.push(`${pad}- tag: ${tag.tag}`);
464
+ lines.push(`${pad} type: ${tag.type}`);
465
+ if (tag.dataType) lines.push(`${pad} dataType: ${tag.dataType}`);
466
+ if (tag.isSlug) {
467
+ lines.push(`${pad} meta:`);
468
+ lines.push(`${pad} slug: "true"`);
469
+ }
470
+ if (tag.repeated) lines.push(`${pad} repeated: true`);
471
+ if (tag.trackBy) lines.push(`${pad} trackBy: ${tag.trackBy}`);
472
+ if (tag.tags && tag.tags.length > 0) {
473
+ lines.push(`${pad} tags:`);
474
+ for (const child of tag.tags) {
475
+ lines.push(tagToYaml(child, indent + 2));
476
+ }
477
+ }
478
+ return lines.join("\n");
479
+ }
480
+ async function generateSchema(contentDir) {
481
+ const files = await fs.readdir(contentDir);
482
+ const dataFile = files.find(
483
+ (f) => f.endsWith(".csv") || f.endsWith(".yaml") || f.endsWith(".yml") || f.endsWith(".json") || f.endsWith(".jsonl")
484
+ );
485
+ if (!dataFile) {
486
+ throw new Error(`No data file found in "${contentDir}"`);
487
+ }
488
+ const rows = await parseDataFile(path.join(contentDir, dataFile));
489
+ if (rows.length === 0) {
490
+ throw new Error(`Data file "${dataFile}" is empty`);
491
+ }
492
+ const tags = inferTags(rows);
493
+ const slugField = pickSlugField(tags);
494
+ if (slugField) {
495
+ const slugTag = tags.find((t) => t.tag === slugField);
496
+ if (slugTag) slugTag.isSlug = true;
497
+ }
498
+ const dirName = path.basename(contentDir);
499
+ const lines = [
500
+ `name: ${dirName}`,
501
+ `description: Auto-generated schema for ${dirName}`,
502
+ "",
503
+ "tags:",
504
+ ...tags.map((t) => tagToYaml(t, 1))
505
+ ];
506
+ return lines.join("\n") + "\n";
507
+ }
508
+ async function generateSchemaCommand(args) {
509
+ const contentDir = args[0];
510
+ if (!contentDir) {
511
+ console.error(
512
+ "Usage: jay-stack run data-files/generate-schema <content-dir>\nExample: jay-stack run data-files/generate-schema content/team"
513
+ );
514
+ process.exit(1);
515
+ }
516
+ const yaml2 = await generateSchema(contentDir);
517
+ const dirName = path.basename(contentDir);
518
+ const outputPath = path.join(contentDir, `${dirName}.jay-contract`);
519
+ try {
520
+ await fs.access(outputPath);
521
+ console.error(`Schema file already exists: ${outputPath}
522
+ Delete it first to regenerate.`);
523
+ process.exit(1);
524
+ } catch {
525
+ }
526
+ await fs.writeFile(outputPath, yaml2, "utf-8");
527
+ console.log(`Generated schema: ${outputPath}`);
528
+ console.log("Review and refine the schema — check slug field, add descriptions, define links.");
529
+ }
530
+ export {
531
+ buildSlugIndex,
532
+ clearFileCache,
533
+ clearParseCache,
534
+ clearSchemaCache,
535
+ dataItem,
536
+ dataList,
537
+ dataPages,
538
+ generateDataItemContract,
539
+ generateDataListContract,
540
+ generateDataPagesContract,
541
+ generateSchema,
542
+ generateSchemaCommand,
543
+ loadSchema,
544
+ parseDataFile,
545
+ resolveReferences
546
+ };
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@jay-framework/data-files",
3
+ "version": "0.23.1",
4
+ "type": "module",
5
+ "description": "Data files plugin for Jay Framework — CSV, YAML, JSON, JSONL data sources with list views, item views, and per-item pages",
6
+ "license": "Apache-2.0",
7
+ "main": "dist/index.js",
8
+ "files": [
9
+ "dist",
10
+ "plugin.yaml",
11
+ "agent-kit"
12
+ ],
13
+ "exports": {
14
+ ".": "./dist/index.js",
15
+ "./client": "./dist/index.client.js",
16
+ "./plugin.yaml": "./plugin.yaml"
17
+ },
18
+ "scripts": {
19
+ "build": "npm run build:server && npm run build:client && npm run build:types",
20
+ "build:client": "vite build",
21
+ "build:server": "vite build --ssr",
22
+ "build:types": "tsup lib/index.ts --dts-only --format esm",
23
+ "build:check-types": "tsc",
24
+ "validate": "jay-stack-cli validate-plugin",
25
+ "clean": "rimraf dist",
26
+ "confirm": "npm run clean && npm run build && npm run build:check-types && npm run test",
27
+ "test": "vitest run"
28
+ },
29
+ "dependencies": {
30
+ "@jay-framework/component": "^0.23.1",
31
+ "@jay-framework/fullstack-component": "^0.23.1",
32
+ "@jay-framework/reactive": "^0.23.1",
33
+ "@jay-framework/runtime": "^0.23.1",
34
+ "@jay-framework/stack-client-runtime": "^0.23.1",
35
+ "@jay-framework/stack-server-runtime": "^0.23.1",
36
+ "js-yaml": "^4.1.0",
37
+ "papaparse": "^5.4.1"
38
+ },
39
+ "devDependencies": {
40
+ "@jay-framework/dev-environment": "^0.23.1",
41
+ "@jay-framework/jay-stack-cli": "^0.23.1",
42
+ "@types/js-yaml": "^4.0.9",
43
+ "@types/node": "^22.15.21",
44
+ "@types/papaparse": "^5.3.14",
45
+ "rimraf": "^5.0.5",
46
+ "tsup": "^8.0.1",
47
+ "typescript": "^5.3.3",
48
+ "vite": "^5.0.11",
49
+ "vitest": "^1.2.1"
50
+ }
51
+ }
package/plugin.yaml ADDED
@@ -0,0 +1,20 @@
1
+ name: data-files
2
+ description: CSV, YAML, JSON, JSONL data sources — list views, item views, and per-item pages
3
+
4
+ dynamic_contracts:
5
+ - prefix: data-pages
6
+ component: dataPages
7
+ generator: generateDataPagesContract
8
+
9
+ - prefix: data-list
10
+ component: dataList
11
+ generator: generateDataListContract
12
+
13
+ - prefix: data-item
14
+ component: dataItem
15
+ generator: generateDataItemContract
16
+
17
+ commands:
18
+ - name: generate-schema
19
+ handler: generateSchema
20
+ description: Auto-generate a .jay-contract schema file from a data file (CSV/YAML/JSON/JSONL)