@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.
- package/agent-kit/designer/data-files-usage.md +211 -0
- package/dist/index.client.js +1 -0
- package/dist/index.d.ts +133 -0
- package/dist/index.js +546 -0
- package/package.json +51 -0
- package/plugin.yaml +20 -0
|
@@ -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
|
+
|
package/dist/index.d.ts
ADDED
|
@@ -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)
|